add builtin scripts support

This commit is contained in:
wh1te909 2020-08-19 05:46:18 +00:00
parent f661912257
commit 666e85adc0
11 changed files with 215 additions and 37 deletions

View File

@ -26,15 +26,22 @@ def get_services():
return [svc.as_dict() for svc in psutil.win_service_iter()]
def run_python_script(filename, timeout):
def run_python_script(filename, timeout, script_type="userdefined"):
python_bin = os.path.join("c:\\salt\\bin", "python.exe")
file_path = os.path.join("c:\\windows\\temp", filename)
__salt__["cp.get_file"](
"salt://scripts/userdefined/{0}".format(filename), file_path
)
return __salt__["cmd.run_all"](
"{0} {1}".format(python_bin, file_path), timeout=timeout
)
if os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
if script_type == "userdefined":
__salt__["cp.get_file"](f"salt://scripts/userdefined/{filename}", file_path)
else:
__salt__["cp.get_file"](f"salt://scripts/{filename}", file_path)
return __salt__["cmd.run_all"](f"{python_bin} {file_path}", timeout=timeout)
def uninstall_agent():

View File

@ -35,7 +35,7 @@ class AutoTaskSerializer(serializers.ModelSerializer):
class TaskRunnerScriptField(serializers.ModelSerializer):
class Meta:
model = Script
fields = ["id", "filepath", "filename", "shell"]
fields = ["id", "filepath", "filename", "shell", "script_type"]
class TaskRunnerGetSerializer(serializers.ModelSerializer):

View File

@ -1,3 +1,5 @@
import os
import subprocess
from time import sleep
from django.core.management.base import BaseCommand
@ -19,3 +21,47 @@ class Command(BaseCommand):
for chunk in chunks:
r = Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules")
sleep(5)
has_old_config = True
rmm_conf = "/etc/nginx/sites-available/rmm.conf"
if os.path.exists(rmm_conf):
with open(rmm_conf) as f:
for line in f:
if "location" and "builtin" in line:
has_old_config = False
break
if has_old_config:
new_conf = """
location /builtin/ {
internal;
add_header "Access-Control-Allow-Origin" "https://rmm.yourwebsite.com";
alias /srv/salt/scripts/;
}
"""
self.stdout.write(self.style.ERROR("*" * 100))
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR(
"WARNING: A recent update requires you to manually edit your nginx config"
)
)
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR("Please add the following location block to ")
+ self.style.WARNING(rmm_conf)
)
self.stdout.write(self.style.SUCCESS(new_conf))
self.stdout.write("\n")
self.stdout.write(
self.style.ERROR(
"Make sure to replace rmm.yourwebsite.com with your domain"
)
)
self.stdout.write(
self.style.ERROR("After editing, restart nginx with the command ")
+ self.style.WARNING("sudo systemctl restart nginx")
)
self.stdout.write("\n")
self.stdout.write(self.style.ERROR("*" * 100))
input("Press Enter to continue...")

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1 on 2020-08-16 20:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='script',
name='script_type',
field=models.CharField(choices=[('userdefined', 'User Defined'), ('builtin', 'Built In')], default='userdefined', max_length=100),
),
]

View File

@ -6,6 +6,11 @@ SCRIPT_SHELLS = [
("python", "Python"),
]
SCRIPT_TYPES = [
("userdefined", "User Defined"),
("builtin", "Built In"),
]
class Script(models.Model):
name = models.CharField(max_length=255)
@ -14,6 +19,9 @@ class Script(models.Model):
shell = models.CharField(
max_length=100, choices=SCRIPT_SHELLS, default="powershell"
)
script_type = models.CharField(
max_length=100, choices=SCRIPT_TYPES, default="userdefined"
)
def __str__(self):
return self.filename
@ -21,11 +29,17 @@ class Script(models.Model):
@property
def filepath(self):
# for the windows agent when using 'salt-call'
return f"salt://scripts//userdefined//{self.filename}"
if self.script_type == "userdefined":
return f"salt://scripts//userdefined//{self.filename}"
else:
return f"salt://scripts//{self.filename}"
@property
def file(self):
return f"/srv/salt/scripts/userdefined/{self.filename}"
if self.script_type == "userdefined":
return f"/srv/salt/scripts/userdefined/{self.filename}"
else:
return f"/srv/salt/scripts/{self.filename}"
@property
def code(self):

View File

@ -1,4 +1,5 @@
import os
from loguru import logger
from django.shortcuts import get_object_or_404
from django.conf import settings
@ -11,6 +12,9 @@ from rest_framework.parsers import FileUploadParser
from .models import Script
from .serializers import ScriptSerializer
from tacticalrmm.utils import notify_error
logger.configure(**settings.LOG_CONFIG)
class GetAddScripts(APIView):
@ -32,6 +36,7 @@ class GetAddScripts(APIView):
"filename": filename,
"description": request.data["description"],
"shell": request.data["shell"],
"script_type": "userdefined", # force all uploads to be userdefined. built in scripts cannot be edited by user
}
serializer = ScriptSerializer(data=data, partial=True)
@ -55,6 +60,10 @@ class GetUpdateDeleteScript(APIView):
def put(self, request, pk, format=None):
script = get_object_or_404(Script, pk=pk)
# this will never trigger but check anyway
if script.script_type == "builtin":
return notify_error("Built in scripts cannot be edited")
data = {
"name": request.data["name"],
"description": request.data["description"],
@ -86,6 +95,10 @@ class GetUpdateDeleteScript(APIView):
def delete(self, request, pk):
script = get_object_or_404(Script, pk=pk)
# this will never trigger but check anyway
if script.script_type == "builtin":
return notify_error("Built in scripts cannot be deleted")
try:
os.remove(script.file)
except OSError:
@ -98,8 +111,22 @@ class GetUpdateDeleteScript(APIView):
@api_view()
def download(request, pk):
script = get_object_or_404(Script, pk=pk)
use_nginx = False
conf = "/etc/nginx/sites-available/rmm.conf"
if settings.DEBUG:
if os.path.exists(conf):
try:
with open(conf) as f:
for line in f.readlines():
if "location" and "builtin" in line:
use_nginx = True
break
except Exception as e:
logger.error(e)
else:
use_nginx = True
if settings.DEBUG or not use_nginx:
with open(script.file, "rb") as f:
response = HttpResponse(f.read(), content_type="text/plain")
response["Content-Disposition"] = f"attachment; filename={script.filename}"
@ -107,5 +134,10 @@ def download(request, pk):
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={script.filename}"
response["X-Accel-Redirect"] = f"/saltscripts/{script.filename}"
response["X-Accel-Redirect"] = (
f"/saltscripts/{script.filename}"
if script.script_type == "userdefined"
else f"/builtin/{script.filename}"
)
return response

View File

@ -11,7 +11,7 @@ AUTH_USER_MODEL = "accounts.User"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.22"
APP_VER = "0.0.23"
# https://github.com/wh1te909/salt
LATEST_SALT_VER = "1.0.3"

View File

@ -46,6 +46,12 @@ http {
alias /srv/salt/scripts/userdefined/;
}
location /builtin/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias /srv/salt/scripts/;
}
location / {
uwsgi_pass tacticalrmm;
include /etc/nginx/uwsgi_params;

View File

@ -377,6 +377,12 @@ server {
alias /srv/salt/scripts/userdefined/;
}
location /builtin/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${frontenddomain}";
alias /srv/salt/scripts/;
}
location / {
uwsgi_pass tacticalrmm;

View File

@ -34,7 +34,7 @@
/>
<q-btn
label="Delete"
:disable="scriptpk === null"
:disable="scriptpk === null || isBuiltInScript(scriptpk)"
dense
flat
push
@ -89,6 +89,7 @@
<q-td>{{ truncateText(props.row.description) }}</q-td>
<q-td>{{ props.row.filename }}</q-td>
<q-td>{{ props.row.shell }}</q-td>
<q-td>{{ props.row.script_type }}</q-td>
</q-tr>
</template>
</q-table>
@ -127,8 +128,8 @@ export default {
code: null,
pagination: {
rowsPerPage: 0,
sortBy: "id",
descending: false
sortBy: "script_type",
descending: true,
},
columns: [
{ name: "id", label: "ID", field: "id" },
@ -137,31 +138,38 @@ export default {
label: "Name",
field: "name",
align: "left",
sortable: true
sortable: true,
},
{
name: "desc",
label: "Description",
field: "description",
align: "left",
sortable: false
sortable: false,
},
{
name: "file",
label: "File",
field: "filename",
align: "left",
sortable: true
sortable: true,
},
{
name: "shell",
label: "Type",
label: "Shell",
field: "shell",
align: "left",
sortable: true
}
sortable: true,
},
{
name: "script_type",
label: "Type",
field: "script_type",
align: "left",
sortable: true,
},
],
visibleColumns: ["name", "desc", "file", "shell"]
visibleColumns: ["name", "desc", "file", "shell", "script_type"],
};
},
methods: {
@ -181,7 +189,7 @@ export default {
title: this.filename,
message: `<pre>${this.code}</pre>`,
html: true,
style: "width: 70vw; max-width: 80vw;"
style: "width: 70vw; max-width: 80vw;",
});
},
deleteScript() {
@ -189,7 +197,7 @@ export default {
.dialog({
title: "Delete script?",
cancel: true,
ok: { label: "Delete", color: "negative" }
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
axios
@ -226,16 +234,23 @@ export default {
},
truncateText(txt) {
return txt.length >= 60 ? txt.substring(0, 60) + "..." : txt;
}
},
isBuiltInScript(pk) {
try {
return this.scripts.find(i => i.id === pk).script_type === "builtin" ? true : false;
} catch (e) {
return false;
}
},
},
computed: {
...mapState({
toggleScriptManager: state => state.toggleScriptManager,
scripts: state => state.scripts
})
scripts: state => state.scripts,
}),
},
mounted() {
this.getScripts();
}
},
};
</script>

View File

@ -10,13 +10,25 @@
<q-card-section class="row">
<div class="col-2">Name:</div>
<div class="col-10">
<q-input outlined dense v-model="script.name" :rules="[ val => !!val || '*Required']" />
<q-input
:disable="isBuiltInScript"
outlined
dense
v-model="script.name"
:rules="[ val => !!val || '*Required']"
/>
</div>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Description:</div>
<div class="col-10">
<q-input outlined dense v-model="script.description" type="textarea" />
<q-input
:disable="isBuiltInScript"
outlined
dense
v-model="script.description"
type="textarea"
/>
</div>
</q-card-section>
<q-card-section class="row">
@ -40,6 +52,7 @@
<div v-if="mode === 'edit'" class="col-10">
<q-file
v-model="script.filename"
:disable="isBuiltInScript"
label="Upload new script version"
stack-label
filled
@ -55,6 +68,7 @@
<q-card-section class="row">
<div class="col-2">Type:</div>
<q-select
:disable="isBuiltInScript"
dense
class="col-10"
outlined
@ -67,7 +81,13 @@
</q-card-section>
<q-card-section class="row items-center">
<q-btn v-if="mode === 'add'" label="Upload" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
<q-btn
v-else-if="mode === 'edit'"
:disable="isBuiltInScript"
label="Edit"
color="primary"
type="submit"
/>
</q-card-section>
</q-form>
</q-card>
@ -75,13 +95,15 @@
<script>
import axios from "axios";
import { mapState } from "vuex";
import mixins from "@/mixins/mixins";
export default {
name: "ScriptModal",
mixins: [mixins],
props: {
scriptpk: Number,
mode: String
mode: String,
},
data() {
return {
@ -90,8 +112,8 @@ export default {
shellOptions: [
{ label: "Powershell", value: "powershell" },
{ label: "Batch (CMD)", value: "cmd" },
{ label: "Python", value: "python" }
]
{ label: "Python", value: "python" },
],
};
},
methods: {
@ -146,12 +168,24 @@ export default {
this.$q.loading.hide();
this.notifyError(e.response.data.non_field_errors, 4000);
});
}
},
},
computed: {
...mapState({
scripts: state => state.scripts,
}),
isBuiltInScript() {
if (this.mode === "edit") {
return this.scripts.find(i => i.id === this.scriptpk).script_type === "builtin" ? true : false;
} else {
return false;
}
},
},
created() {
if (this.mode === "edit") {
this.getScript();
}
}
},
};
</script>