rework deployments ui, implement client/site permisssions, and tests
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-28 00:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0018_auto_20211010_0249'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='deployment',
|
||||
name='client',
|
||||
),
|
||||
]
|
||||
@@ -265,9 +265,6 @@ class Deployment(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
uid = models.UUIDField(primary_key=False, default=uuid.uuid4, editable=False)
|
||||
client = models.ForeignKey(
|
||||
"clients.Client", related_name="deployclients", on_delete=models.CASCADE
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
"clients.Site", related_name="deploysites", on_delete=models.CASCADE
|
||||
)
|
||||
@@ -286,6 +283,10 @@ class Deployment(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.client} - {self.site} - {self.mon_type}"
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
return self.site.client
|
||||
|
||||
|
||||
class ClientCustomField(models.Model):
|
||||
client = models.ForeignKey(
|
||||
|
||||
@@ -4,6 +4,7 @@ from itertools import cycle
|
||||
|
||||
from model_bakery import baker
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
@@ -432,8 +433,8 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_generate_deployment(self):
|
||||
# TODO complete this
|
||||
@patch("tacticalrmm.utils.generate_winagent_exe", return_value=Response("ok"))
|
||||
def test_generate_deployment(self, post):
|
||||
url = "/clients/asdkj234kasdasjd-asdkj234-asdk34-sad/deploy/"
|
||||
|
||||
r = self.client.get(url)
|
||||
@@ -445,6 +446,14 @@ class TestClientViews(TacticalTestCase):
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# test valid download
|
||||
deployment = baker.make("clients.Deployment", install_flags={"rdp": True, "ping": False, "power": False})
|
||||
|
||||
url = f"/clients/{deployment.uid}/deploy/"
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class TestClientPermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
@@ -454,7 +463,7 @@ class TestClientPermissions(TacticalTestCase):
|
||||
def test_get_clients_permissions(self):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
url = f"{base_url}/"
|
||||
|
||||
@@ -471,17 +480,17 @@ class TestClientPermissions(TacticalTestCase):
|
||||
|
||||
# all agents should be returned
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 5)
|
||||
self.assertEqual(len(response.data), 5) # type: ignore
|
||||
|
||||
# limit user to specific client. only 1 client should be returned
|
||||
user.role.can_view_clients.set([clients[3]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(len(response.data), 1) # type: ignore
|
||||
|
||||
# 2 should be returned now
|
||||
user.role.can_view_clients.set([clients[0], clients[1]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertEqual(len(response.data), 2) # type: ignore
|
||||
|
||||
# limit to a specific site. The site shouldn't be in client returned sites
|
||||
sites = baker.make("clients.Site", client=clients[4], _quantity=3)
|
||||
@@ -490,8 +499,8 @@ class TestClientPermissions(TacticalTestCase):
|
||||
|
||||
user.role.can_view_sites.set([sites[0]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
for client in response.data:
|
||||
self.assertEqual(len(response.data), 3) # type: ignore
|
||||
for client in response.data: # type: ignore
|
||||
if client["id"] == clients[0].id:
|
||||
self.assertEqual(len(client["sites"]), 4)
|
||||
elif client["id"] == clients[1].id:
|
||||
@@ -521,7 +530,7 @@ class TestClientPermissions(TacticalTestCase):
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
@@ -536,7 +545,7 @@ class TestClientPermissions(TacticalTestCase):
|
||||
def test_get_edit_delete_clients_permissions(self, delete):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
client = baker.make("clients.Client")
|
||||
unauthorized_client = baker.make("clients.Client")
|
||||
@@ -572,7 +581,7 @@ class TestClientPermissions(TacticalTestCase):
|
||||
def test_get_sites_permissions(self):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
url = f"{base_url}/sites/"
|
||||
|
||||
@@ -590,28 +599,28 @@ class TestClientPermissions(TacticalTestCase):
|
||||
|
||||
# all sites should be returned
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 10)
|
||||
self.assertEqual(len(response.data), 10) # type: ignore
|
||||
|
||||
# limit user to specific site. only 1 site should be returned
|
||||
user.role.can_view_sites.set([sites[3]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(len(response.data), 1) # type: ignore
|
||||
|
||||
# 2 should be returned now
|
||||
user.role.can_view_sites.set([sites[0], sites[1]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertEqual(len(response.data), 2) # type: ignore
|
||||
|
||||
# check if limiting user to client works
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([clients[0]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 4)
|
||||
self.assertEqual(len(response.data), 4) # type: ignore
|
||||
|
||||
# add a site to see if the results still work
|
||||
user.role.can_view_sites.set([sites[1], sites[0]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 5)
|
||||
self.assertEqual(len(response.data), 5) # type: ignore
|
||||
|
||||
# make sure superusers work
|
||||
self.check_authorized_superuser("get", url)
|
||||
@@ -632,7 +641,7 @@ class TestClientPermissions(TacticalTestCase):
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
@@ -658,7 +667,7 @@ class TestClientPermissions(TacticalTestCase):
|
||||
def test_get_edit_delete_sites_permissions(self, delete):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
site = baker.make("clients.Site")
|
||||
unauthorized_site = baker.make("clients.Site")
|
||||
@@ -698,3 +707,135 @@ class TestClientPermissions(TacticalTestCase):
|
||||
# make sure superusers work
|
||||
for method in methods:
|
||||
self.check_authorized_superuser(method, f"{base_url}/{unauthorized_site.id}/")
|
||||
|
||||
def test_get_pendingactions_permissions(self):
|
||||
url = f"{base_url}/deployments/"
|
||||
|
||||
site = baker.make("clients.Site")
|
||||
other_site = baker.make("clients.Site")
|
||||
deployments = baker.make("clients.Deployment", site=site, _quantity=5)
|
||||
other_deployments = baker.make("clients.Deployment", site=other_site, _quantity=7)
|
||||
|
||||
# test getting all deployments
|
||||
# make sure superusers work
|
||||
self.check_authorized_superuser("get", url)
|
||||
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# user with empty role should fail
|
||||
self.check_not_authorized("get", url)
|
||||
|
||||
# add can_list_sites roles and should succeed
|
||||
user.role.can_list_deployments = True
|
||||
user.role.save()
|
||||
|
||||
# all sites should be returned
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 12) # type: ignore
|
||||
|
||||
# limit user to specific site. only 1 site should be returned
|
||||
user.role.can_view_sites.set([site])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 5) # type: ignore
|
||||
|
||||
# all should be returned now
|
||||
user.role.can_view_clients.set([other_site.client])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 12) # type: ignore
|
||||
|
||||
# check if limiting user to client works
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([other_site.client])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 7) # type: ignore
|
||||
|
||||
@patch("clients.models.Deployment.save")
|
||||
def test_add_deployments_permissions(self, save):
|
||||
site = baker.make("clients.Site")
|
||||
unauthorized_site = baker.make("clients.Site")
|
||||
data = {
|
||||
"site": site.id,
|
||||
}
|
||||
|
||||
# test adding to unauthorized client
|
||||
unauthorized_data = {
|
||||
"site": unauthorized_site.id,
|
||||
}
|
||||
|
||||
url = f"{base_url}/deployments/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_manage_deployments = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
# limit to client and test
|
||||
user.role.can_view_clients.set([site.client])
|
||||
self.check_authorized("post", url, data)
|
||||
self.check_not_authorized("post", url, unauthorized_data)
|
||||
|
||||
# limit to site and test
|
||||
user.role.can_view_clients.clear()
|
||||
user.role.can_view_sites.set([site])
|
||||
self.check_authorized("post", url, data)
|
||||
self.check_not_authorized("post", url, unauthorized_data)
|
||||
|
||||
@patch("clients.models.Deployment.delete")
|
||||
def test_delete_deployments_permissions(self, delete):
|
||||
site = baker.make("clients.Site")
|
||||
unauthorized_site = baker.make("clients.Site")
|
||||
deployment = baker.make("clients.Deployment", site=site)
|
||||
unauthorized_deployment = baker.make("clients.Deployment", site=unauthorized_site)
|
||||
|
||||
url = f"{base_url}/deployments/{deployment.id}/"
|
||||
unauthorized_url = f"{base_url}/deployments/{unauthorized_deployment.id}/"
|
||||
|
||||
# make sure superusers work
|
||||
self.check_authorized_superuser("delete", url)
|
||||
self.check_authorized_superuser("delete", unauthorized_url)
|
||||
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# make sure user with empty role is unauthorized
|
||||
self.check_not_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
# add correct roles for view edit and delete
|
||||
user.role.can_manage_deployments = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_authorized("delete", unauthorized_url)
|
||||
|
||||
# test limiting users to clients and sites
|
||||
|
||||
# limit to site
|
||||
user.role.can_view_sites.set([site])
|
||||
|
||||
# recreate deployment since it is being deleted even though I am mocking delete on Deployment model???
|
||||
unauthorized_deployment = baker.make("clients.Deployment", site=unauthorized_site)
|
||||
unauthorized_url = f"{base_url}/deployments/{unauthorized_deployment.id}/"
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
# test limit to only client
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([site.client])
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
@@ -8,10 +8,12 @@ from django.utils import timezone as djangotime
|
||||
from rest_framework.permissions import AllowAny, 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 core.models import CoreSettings
|
||||
from tacticalrmm.utils import notify_error
|
||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
|
||||
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
from .permissions import (
|
||||
@@ -143,6 +145,10 @@ class GetAddSites(APIView):
|
||||
return Response(SiteSerializer(sites, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
|
||||
if not _has_perm_on_client(request.user, request.data["site"]["client"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
serializer = SiteSerializer(data=request.data["site"])
|
||||
serializer.is_valid(raise_exception=True)
|
||||
site = serializer.save()
|
||||
@@ -233,16 +239,18 @@ class AgentDeployment(APIView):
|
||||
permission_classes = [IsAuthenticated, DeploymentPerms]
|
||||
|
||||
def get(self, request):
|
||||
deps = Deployment.objects.all()
|
||||
deps = Deployment.objects.filter_by_role(request.user)
|
||||
return Response(DeploymentSerializer(deps, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
from knox.models import AuthToken
|
||||
from accounts.models import User
|
||||
|
||||
client = get_object_or_404(Client, pk=request.data["client"])
|
||||
site = get_object_or_404(Site, pk=request.data["site"])
|
||||
|
||||
if not _has_perm_on_site(request.user, site.pk):
|
||||
raise PermissionDenied()
|
||||
|
||||
installer_user = User.objects.filter(is_installer_user=True).first()
|
||||
|
||||
expires = dt.datetime.strptime(
|
||||
@@ -259,7 +267,6 @@ class AgentDeployment(APIView):
|
||||
}
|
||||
|
||||
Deployment(
|
||||
client=client,
|
||||
site=site,
|
||||
expiry=expires,
|
||||
mon_type=request.data["agenttype"],
|
||||
@@ -268,17 +275,21 @@ class AgentDeployment(APIView):
|
||||
token_key=token,
|
||||
install_flags=flags,
|
||||
).save()
|
||||
return Response("ok")
|
||||
return Response("The deployment was added successfully")
|
||||
|
||||
def delete(self, request, pk):
|
||||
d = get_object_or_404(Deployment, pk=pk)
|
||||
|
||||
if not _has_perm_on_site(request.user, d.site.pk):
|
||||
raise PermissionDenied()
|
||||
|
||||
try:
|
||||
d.auth_token.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
d.delete()
|
||||
return Response("ok")
|
||||
return Response("The deployment was deleted")
|
||||
|
||||
|
||||
class GenerateAgent(APIView):
|
||||
|
||||
@@ -19,7 +19,7 @@ class PermissionQuerySet(models.QuerySet):
|
||||
return self
|
||||
|
||||
# checks which sites and clients the user has access to and filters agents
|
||||
if model_name == "Agent":
|
||||
if model_name in ["Agent", "Deployment"]:
|
||||
if can_view_clients:
|
||||
clients_queryset = models.Q(site__client__in=can_view_clients)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from "axios"
|
||||
|
||||
const baseUrl = "/clients"
|
||||
|
||||
// client endpoints
|
||||
export async function fetchClients() {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/`)
|
||||
@@ -31,6 +32,7 @@ export async function removeClient(id, params = {}) {
|
||||
return data
|
||||
}
|
||||
|
||||
// site endpoints
|
||||
export async function fetchSites() {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/sites/`)
|
||||
@@ -59,3 +61,21 @@ export async function removeSite(id, params = {}) {
|
||||
const { data } = await axios.delete(`${baseUrl}/sites/${id}/`, { params: params })
|
||||
return data
|
||||
}
|
||||
|
||||
// deployment endpoints
|
||||
export async function fetchDeployments() {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/deployments/`)
|
||||
return data
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
export async function saveDeployment(payload) {
|
||||
const { data } = await axios.post(`${baseUrl}/deployments/`, payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function removeDeployment(id, params = {}) {
|
||||
const { data } = await axios.delete(`${baseUrl}/deployments/${id}/`, { params: params })
|
||||
return data
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<q-card style="min-width: 70vw">
|
||||
<q-bar>
|
||||
<q-btn @click="getDeployments" class="q-mr-sm" dense flat push icon="refresh" />
|
||||
<q-space />Manage Deployments
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary" />
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div class="row">
|
||||
<div class="q-pa-sm q-ml-sm">
|
||||
<q-btn color="primary" icon="add" label="New" @click="showNewDeployment = true" />
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="audit-mgr-tbl-sticky"
|
||||
binary-state-sort
|
||||
virtual-scroll
|
||||
:rows="deployments"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
row-key="id"
|
||||
v-model:pagination="pagination"
|
||||
no-data-label="No Deployments"
|
||||
>
|
||||
<template v-slot:body="props">
|
||||
<q-tr>
|
||||
<q-td key="client" :props="props">{{ props.row.client_name }}</q-td>
|
||||
<q-td key="site" :props="props">{{ props.row.site_name }}</q-td>
|
||||
<q-td key="mon_type" :props="props">{{ props.row.mon_type }}</q-td>
|
||||
<q-td key="arch" :props="props"
|
||||
><span v-if="props.row.arch === '64'">64 bit</span><span v-else>32 bit</span></q-td
|
||||
>
|
||||
<q-td key="expiry" :props="props">{{ props.row.expiry }}</q-td>
|
||||
<q-td key="created" :props="props">{{ props.row.created }}</q-td>
|
||||
<q-td key="flags" :props="props"
|
||||
><q-badge color="grey-8" label="View Flags" />
|
||||
<q-tooltip style="font-size: 12px">{{ props.row.install_flags }}</q-tooltip>
|
||||
</q-td>
|
||||
<q-td key="link" :props="props"
|
||||
><q-btn size="sm" color="primary" icon="content_copy" label="Copy" @click="copyLink(props)"
|
||||
/></q-td>
|
||||
<q-td key="delete" :props="props"
|
||||
><q-btn size="sm" color="negative" icon="delete" @click="deleteDeployment(props.row.id)"
|
||||
/></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
<q-dialog v-model="showNewDeployment">
|
||||
<NewDeployment @close="showNewDeployment = false" @add="getDeployments" />
|
||||
</q-dialog>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import NewDeployment from "@/components/modals/clients/NewDeployment";
|
||||
import { copyToClipboard } from "quasar";
|
||||
import { getBaseUrl } from "@/boot/axios";
|
||||
|
||||
export default {
|
||||
name: "Deployment",
|
||||
mixins: [mixins],
|
||||
components: { NewDeployment },
|
||||
data() {
|
||||
return {
|
||||
showNewDeployment: false,
|
||||
deployments: [],
|
||||
columns: [
|
||||
{ name: "id", field: "id" },
|
||||
{ name: "uid", field: "uid" },
|
||||
{ name: "clientid", field: "client_id" },
|
||||
{ name: "siteid", field: "site_id" },
|
||||
{ name: "client", label: "Client", field: "client_name", align: "left", sortable: true },
|
||||
{ name: "site", label: "Site", field: "site_name", align: "left", sortable: true },
|
||||
{ name: "mon_type", label: "Type", field: "mon_type", align: "left", sortable: true },
|
||||
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
|
||||
{ name: "expiry", label: "Expiry", field: "expiry", align: "left", sortable: true },
|
||||
{ name: "created", label: "Created", field: "created", align: "left", sortable: true },
|
||||
{ name: "flags", label: "Flags", field: "install_flags", align: "left" },
|
||||
{ name: "link", label: "Download Link", align: "left" },
|
||||
{ name: "delete", label: "Delete", align: "left" },
|
||||
],
|
||||
visibleColumns: ["client", "site", "mon_type", "arch", "expiry", "created", "flags", "link", "delete"],
|
||||
|
||||
pagination: {
|
||||
rowsPerPage: 50,
|
||||
sortBy: "id",
|
||||
descending: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getDeployments() {
|
||||
this.$axios
|
||||
.get("/clients/deployments/")
|
||||
.then(r => {
|
||||
this.deployments = r.data;
|
||||
})
|
||||
.catch(e => {});
|
||||
},
|
||||
deleteDeployment(pk) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Delete deployment?",
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$axios
|
||||
.delete(`/clients/deployments/${pk}/`)
|
||||
.then(r => {
|
||||
this.getDeployments();
|
||||
this.notifySuccess("Deployment deleted");
|
||||
})
|
||||
.catch(e => {});
|
||||
});
|
||||
},
|
||||
copyLink(props) {
|
||||
const api = getBaseUrl();
|
||||
copyToClipboard(`${api}/clients/${props.row.uid}/deploy/`).then(() => {
|
||||
this.notifySuccess("Link copied to clipboard", 1500);
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getDeployments();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -48,7 +48,7 @@
|
||||
<q-item clickable v-close-popup @click="showInstallAgent = true">
|
||||
<q-item-section>Install Agent</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showDeployment = true">
|
||||
<q-item clickable v-close-popup @click="showDeployments">
|
||||
<q-item-section>Manage Deployments</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showUpdateAgentsModal = true">
|
||||
@@ -163,10 +163,6 @@
|
||||
<AdminManager @close="showAdminManager = false" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
<!-- Agent Deployment -->
|
||||
<q-dialog v-model="showDeployment">
|
||||
<Deployment @close="showDeployment = false" />
|
||||
</q-dialog>
|
||||
<!-- Server Maintenance -->
|
||||
<q-dialog v-model="showServerMaintenance">
|
||||
<ServerMaintenance @close="showMaintenance = false" />
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
171
web/src/components/clients/Deployment.vue
Normal file
171
web/src/components/clients/Deployment.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card style="min-width: 70vw; height: 70vh">
|
||||
<q-bar>
|
||||
<q-btn @click="getDeployments" class="q-mr-sm" dense flat push icon="refresh" />
|
||||
Manage Deployments
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary" />
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-table
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="audit-mgr-tbl-sticky"
|
||||
style="max-height: 65vh"
|
||||
binary-state-sort
|
||||
virtual-scroll
|
||||
:rows="deployments"
|
||||
:columns="columns"
|
||||
:rows-per-page-options="[0]"
|
||||
row-key="id"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'id', descending: true }"
|
||||
no-data-label="No Deployments"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-btn dense flat icon="add" label="New" @click="showAddDeployment" />
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props" class="cursor-pointer" @dblclick="copyToClipboard(props.row)">
|
||||
<q-menu context-menu auto-close>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable @click="deleteDeployment(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
</q-item-section>
|
||||
<q-item-section>Delete</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable>
|
||||
<q-item-section>Close</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<q-td key="client" :props="props">{{ props.row.client_name }}</q-td>
|
||||
<q-td key="site" :props="props">{{ props.row.site_name }}</q-td>
|
||||
<q-td key="mon_type" :props="props">{{ props.row.mon_type }}</q-td>
|
||||
<q-td key="arch" :props="props"
|
||||
><span v-if="props.row.arch === '64'">64 bit</span><span v-else>32 bit</span></q-td
|
||||
>
|
||||
<q-td key="expiry" :props="props">{{ props.row.expiry }}</q-td>
|
||||
<q-td key="created" :props="props">{{ props.row.created }}</q-td>
|
||||
<q-td key="flags" :props="props"
|
||||
><q-badge color="grey-8" label="View Flags" />
|
||||
<q-tooltip style="font-size: 12px">{{ props.row.install_flags }}</q-tooltip>
|
||||
</q-td>
|
||||
<q-td key="link" :props="props">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="sm"
|
||||
color="primary"
|
||||
icon="content_copy"
|
||||
label="Copy"
|
||||
@click="copyLink(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useQuasar, useDialogPluginComponent, copyToClipboard } from "quasar";
|
||||
import { fetchDeployments, removeDeployment } from "@/api/clients";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { getBaseUrl } from "@/boot/axios";
|
||||
|
||||
// ui imports
|
||||
import NewDeployment from "@/components/clients/NewDeployment";
|
||||
|
||||
// static data
|
||||
const columns = [
|
||||
{ name: "client", label: "Client", field: "client_name", align: "left", sortable: true },
|
||||
{ name: "site", label: "Site", field: "site_name", align: "left", sortable: true },
|
||||
{ name: "mon_type", label: "Type", field: "mon_type", align: "left", sortable: true },
|
||||
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
|
||||
{ name: "expiry", label: "Expiry", field: "expiry", align: "left", sortable: true },
|
||||
{ name: "created", label: "Created", field: "created", align: "left", sortable: true },
|
||||
{ name: "flags", label: "Flags", field: "install_flags", align: "left" },
|
||||
{ name: "link", label: "Download Link", align: "left" },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "Deployment",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
setup(props) {
|
||||
// quasar dialog setup
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
const $q = useQuasar();
|
||||
|
||||
// deployment logic
|
||||
const deployments = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
async function getDeployments() {
|
||||
loading.value = true;
|
||||
deployments.value = await fetchDeployments();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function deleteDeployment(deployment) {
|
||||
$q.dialog({
|
||||
title: "Delete deployment?",
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
}).onOk(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await removeDeployment(deployment.id);
|
||||
notifySuccess(result);
|
||||
await getDeployments();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function copyLink(deployment) {
|
||||
const api = getBaseUrl();
|
||||
copyToClipboard(`${api}/clients/${deployment.uid}/deploy/`).then(() => {
|
||||
notifySuccess("Link copied to clipboard", 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function showAddDeployment() {
|
||||
$q.dialog({
|
||||
component: NewDeployment,
|
||||
}).onOk(getDeployments);
|
||||
}
|
||||
|
||||
onMounted(getDeployments);
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
deployments,
|
||||
loading,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
// mehtods
|
||||
getDeployments,
|
||||
deleteDeployment,
|
||||
showAddDeployment,
|
||||
copyLink,
|
||||
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
132
web/src/components/clients/NewDeployment.vue
Normal file
132
web/src/components/clients/NewDeployment.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card style="width: 40vw">
|
||||
<q-bar>
|
||||
Add Deployment
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary" />
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit="submit">
|
||||
<q-card-section>
|
||||
<tactical-dropdown outlined label="Site" v-model="state.site" :options="siteOptions" mapOptions />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-pl-sm">Agent Type</div>
|
||||
<q-radio v-model="state.agenttype" val="server" label="Server" @update:model-value="power = false" />
|
||||
<q-radio v-model="state.agenttype" val="workstation" label="Workstation" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input label="Expiry" dense filled v-model="state.expires" hint="Agent timezone will be used">
|
||||
<template v-slot:append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="state.expires" mask="YYYY-MM-DD HH:mm">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
<q-icon name="access_time" class="cursor-pointer">
|
||||
<q-popup-proxy transition-show="scale" transition-hide="scale">
|
||||
<q-time v-model="state.expires" mask="YYYY-MM-DD HH:mm">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
||||
</div>
|
||||
</q-time>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<q-checkbox v-model="state.rdp" dense label="Enable RDP" />
|
||||
<q-checkbox v-model="state.ping" dense label="Enable Ping" />
|
||||
<q-checkbox
|
||||
v-model="state.power"
|
||||
dense
|
||||
v-show="state.agenttype === 'workstation'"
|
||||
label="Disable sleep/hibernate"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-pl-sm">OS</div>
|
||||
<q-radio v-model="state.arch" val="64" label="64 bit" />
|
||||
<q-radio v-model="state.arch" val="32" label="32 bit" />
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn dense flat label="Cancel" v-close-popup />
|
||||
<q-btn :loading="loading" dense flat label="Create" color="primary" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useSiteDropdown } from "@/composables/clients";
|
||||
import { saveDeployment } from "@/api/clients";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { date } from "quasar";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
|
||||
export default {
|
||||
name: "NewDeployment",
|
||||
components: {
|
||||
TacticalDropdown,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
setup(props) {
|
||||
// setup quasar dialog
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// setup site dropdown
|
||||
const { siteOptions } = useSiteDropdown(true);
|
||||
|
||||
// add deployment logic
|
||||
const state = ref({
|
||||
site: null,
|
||||
expires: date.formatDate(new Date().setDate(new Date().getDate() + 30), "YYYY-MM-DD HH:mm"),
|
||||
agenttype: "server",
|
||||
power: false,
|
||||
rdp: false,
|
||||
ping: false,
|
||||
arch: "64",
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await saveDeployment(state.value);
|
||||
notifySuccess(result);
|
||||
onDialogOK();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
state,
|
||||
loading,
|
||||
siteOptions,
|
||||
|
||||
// methods
|
||||
submit,
|
||||
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,150 +0,0 @@
|
||||
<template>
|
||||
<q-card style="min-width: 25vw">
|
||||
<q-card-section class="row">
|
||||
<q-card-actions align="left">
|
||||
<div class="text-h6">Create a Deployment</div>
|
||||
</q-card-actions>
|
||||
<q-space />
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat round dense icon="close" />
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form @submit.prevent="create">
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
options-dense
|
||||
label="Client"
|
||||
v-model="client"
|
||||
:options="client_options"
|
||||
@update:model-value="site = sites[0].value"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<q-select dense options-dense outlined label="Site" v-model="site" :options="sites" map-options emit-value />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio v-model="agenttype" val="server" label="Server" @update:model-value="power = false" />
|
||||
<q-radio v-model="agenttype" val="workstation" label="Workstation" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
Expiry
|
||||
<div class="q-gutter-sm">
|
||||
<q-input filled v-model="datetime">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="datetime" mask="YYYY-MM-DD HH:mm" />
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<q-icon name="access_time" class="cursor-pointer">
|
||||
<q-popup-proxy transition-show="scale" transition-hide="scale">
|
||||
<q-time v-model="datetime" mask="YYYY-MM-DD HH:mm" />
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-checkbox v-model="rdp" dense label="Enable RDP" />
|
||||
<q-checkbox v-model="ping" dense label="Enable Ping" />
|
||||
<q-checkbox v-model="power" dense v-show="agenttype === 'workstation'" label="Disable sleep/hibernate" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
OS
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio v-model="arch" val="64" label="64 bit" />
|
||||
<q-radio v-model="arch" val="32" label="32 bit" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="left">
|
||||
<q-btn label="Create" color="primary" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { date } from "quasar";
|
||||
|
||||
export default {
|
||||
name: "NewDeployment",
|
||||
emits: ["close", "add"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
client_options: [],
|
||||
datetime: null,
|
||||
client: null,
|
||||
site: null,
|
||||
agenttype: "server",
|
||||
power: false,
|
||||
rdp: false,
|
||||
ping: false,
|
||||
arch: "64",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
create() {
|
||||
const data = {
|
||||
client: this.client.value,
|
||||
site: this.site,
|
||||
expires: this.datetime,
|
||||
agenttype: this.agenttype,
|
||||
power: this.power ? 1 : 0,
|
||||
rdp: this.rdp ? 1 : 0,
|
||||
ping: this.ping ? 1 : 0,
|
||||
arch: this.arch,
|
||||
};
|
||||
this.$axios
|
||||
.post("/clients/deployments/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$emit("add");
|
||||
this.notifySuccess("Deployment added");
|
||||
})
|
||||
.catch(e => {});
|
||||
},
|
||||
getCurrentDate() {
|
||||
let d = new Date();
|
||||
d.setDate(d.getDate() + 30);
|
||||
this.datetime = date.formatDate(d, "YYYY-MM-DD HH:mm");
|
||||
},
|
||||
getClients() {
|
||||
this.$q.loading.show();
|
||||
this.$axios
|
||||
.get("/clients/")
|
||||
.then(r => {
|
||||
this.client_options = this.formatClientOptions(r.data);
|
||||
this.client = this.client_options[0];
|
||||
this.site = this.formatSiteOptions(this.client.sites)[0].value;
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sites() {
|
||||
return this.client !== null ? this.formatSiteOptions(this.client.sites) : [];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getCurrentDate();
|
||||
this.getClients();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user