mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +00:00
Improve acl model
This commit is contained in:
41
vpn/admin.py
41
vpn/admin.py
@@ -195,20 +195,20 @@ class LastAccessFilter(admin.SimpleListFilter):
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
if self.value() == 'never':
|
if self.value() == 'never':
|
||||||
# Links that have no access logs
|
# Links that have never been accessed
|
||||||
return queryset.filter(last_access__isnull=True)
|
return queryset.filter(last_access_time__isnull=True)
|
||||||
elif self.value() == 'week':
|
elif self.value() == 'week':
|
||||||
# Links accessed in the last week
|
# Links accessed in the last week
|
||||||
week_ago = timezone.now() - timedelta(days=7)
|
week_ago = timezone.now() - timedelta(days=7)
|
||||||
return queryset.filter(last_access__gte=week_ago)
|
return queryset.filter(last_access_time__gte=week_ago)
|
||||||
elif self.value() == 'month':
|
elif self.value() == 'month':
|
||||||
# Links accessed in the last month
|
# Links accessed in the last month
|
||||||
month_ago = timezone.now() - timedelta(days=30)
|
month_ago = timezone.now() - timedelta(days=30)
|
||||||
return queryset.filter(last_access__gte=month_ago)
|
return queryset.filter(last_access_time__gte=month_ago)
|
||||||
elif self.value() == 'old':
|
elif self.value() == 'old':
|
||||||
# Links not accessed for more than 3 months
|
# Links not accessed for more than 3 months
|
||||||
three_months_ago = timezone.now() - timedelta(days=90)
|
three_months_ago = timezone.now() - timedelta(days=90)
|
||||||
return queryset.filter(last_access__lt=three_months_ago)
|
return queryset.filter(last_access_time__lt=three_months_ago)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@admin.register(Server)
|
@admin.register(Server)
|
||||||
@@ -683,17 +683,6 @@ class ACLLinkAdmin(admin.ModelAdmin):
|
|||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
qs = qs.select_related('acl__user', 'acl__server')
|
qs = qs.select_related('acl__user', 'acl__server')
|
||||||
|
|
||||||
# Add last access annotation
|
|
||||||
qs = qs.annotate(
|
|
||||||
last_access=Subquery(
|
|
||||||
AccessLog.objects.filter(
|
|
||||||
user=OuterRef('acl__user__username'),
|
|
||||||
server=OuterRef('acl__server__name')
|
|
||||||
).order_by('-timestamp').values('timestamp')[:1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@admin.display(description='Link', ordering='link')
|
@admin.display(description='Link', ordering='link')
|
||||||
@@ -723,15 +712,15 @@ class ACLLinkAdmin(admin.ModelAdmin):
|
|||||||
return obj.comment[:50] + '...' if len(obj.comment) > 50 else obj.comment
|
return obj.comment[:50] + '...' if len(obj.comment) > 50 else obj.comment
|
||||||
return '-'
|
return '-'
|
||||||
|
|
||||||
@admin.display(description='Last Access', ordering='last_access')
|
@admin.display(description='Last Access', ordering='last_access_time')
|
||||||
def last_access_display(self, obj):
|
def last_access_display(self, obj):
|
||||||
if hasattr(obj, 'last_access') and obj.last_access:
|
if obj.last_access_time:
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
local_time = localtime(obj.last_access)
|
local_time = localtime(obj.last_access_time)
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
diff = now - obj.last_access
|
diff = now - obj.last_access_time
|
||||||
|
|
||||||
# Color coding based on age
|
# Color coding based on age
|
||||||
if diff <= timedelta(days=7):
|
if diff <= timedelta(days=7):
|
||||||
@@ -794,17 +783,17 @@ class ACLLinkAdmin(admin.ModelAdmin):
|
|||||||
# Add summary statistics to the changelist
|
# Add summary statistics to the changelist
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
|
|
||||||
# Get queryset with annotations for statistics
|
# Get queryset for statistics
|
||||||
queryset = self.get_queryset(request)
|
queryset = self.get_queryset(request)
|
||||||
|
|
||||||
total_links = queryset.count()
|
total_links = queryset.count()
|
||||||
never_accessed = queryset.filter(last_access__isnull=True).count()
|
never_accessed = queryset.filter(last_access_time__isnull=True).count()
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
three_months_ago = timezone.now() - timedelta(days=90)
|
three_months_ago = timezone.now() - timedelta(days=90)
|
||||||
old_links = queryset.filter(
|
old_links = queryset.filter(
|
||||||
Q(last_access__lt=three_months_ago) | Q(last_access__isnull=True)
|
Q(last_access_time__lt=three_months_ago) | Q(last_access_time__isnull=True)
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
extra_context.update({
|
extra_context.update({
|
||||||
@@ -817,7 +806,7 @@ class ACLLinkAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_ordering(self, request):
|
def get_ordering(self, request):
|
||||||
"""Allow sorting by annotated fields"""
|
"""Allow sorting by annotated fields"""
|
||||||
# Handle sorting by last_access if requested
|
# Handle sorting by last_access_time if requested
|
||||||
order_var = request.GET.get('o')
|
order_var = request.GET.get('o')
|
||||||
if order_var:
|
if order_var:
|
||||||
try:
|
try:
|
||||||
@@ -825,9 +814,9 @@ class ACLLinkAdmin(admin.ModelAdmin):
|
|||||||
# Check if this corresponds to the last_access column (index 4 in list_display)
|
# Check if this corresponds to the last_access column (index 4 in list_display)
|
||||||
if field_index == 4: # last_access_display is at index 4
|
if field_index == 4: # last_access_display is at index 4
|
||||||
if order_var.startswith('-'):
|
if order_var.startswith('-'):
|
||||||
return ['-last_access']
|
return ['-last_access_time']
|
||||||
else:
|
else:
|
||||||
return ['last_access']
|
return ['last_access_time']
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
18
vpn/migrations/0003_acllink_last_access_time.py
Normal file
18
vpn/migrations/0003_acllink_last_access_time.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated migration for adding last_access_time field
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('vpn', '0002_taskexecutionlog'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='acllink',
|
||||||
|
name='last_access_time',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Last time this link was accessed', null=True),
|
||||||
|
),
|
||||||
|
]
|
@@ -130,6 +130,7 @@ class ACLLink(models.Model):
|
|||||||
acl = models.ForeignKey(ACL, related_name='links', on_delete=models.CASCADE)
|
acl = models.ForeignKey(ACL, related_name='links', on_delete=models.CASCADE)
|
||||||
comment = models.TextField(default="", blank=True, help_text="ACL link comment, device name, etc...")
|
comment = models.TextField(default="", blank=True, help_text="ACL link comment, device name, etc...")
|
||||||
link = models.CharField(max_length=1024, default="", unique=True, blank=True, null=True, verbose_name="Access link", help_text="Access link to get dynamic configuration")
|
link = models.CharField(max_length=1024, default="", unique=True, blank=True, null=True, verbose_name="Access link", help_text="Access link to get dynamic configuration")
|
||||||
|
last_access_time = models.DateTimeField(null=True, blank=True, help_text="Last time this link was accessed")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.link == "":
|
if self.link == "":
|
||||||
|
95
vpn/views.py
95
vpn/views.py
@@ -1,8 +1,7 @@
|
|||||||
def userPortal(request, user_hash):
|
def userPortal(request, user_hash):
|
||||||
"""HTML portal for user to view their VPN access links and server information"""
|
"""HTML portal for user to view their VPN access links and server information"""
|
||||||
from .models import User, ACLLink, AccessLog
|
from .models import User, ACLLink
|
||||||
import logging
|
import logging
|
||||||
from django.db.models import Count, Q
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,78 +19,10 @@ def userPortal(request, user_hash):
|
|||||||
# Get all ACL links for the user with server information
|
# Get all ACL links for the user with server information
|
||||||
acl_links = ACLLink.objects.filter(acl__user=user).select_related('acl__server', 'acl')
|
acl_links = ACLLink.objects.filter(acl__user=user).select_related('acl__server', 'acl')
|
||||||
|
|
||||||
# Get connection statistics for all user's links
|
|
||||||
# Count successful connections for each specific link
|
|
||||||
connection_stats = {}
|
|
||||||
recent_connection_stats = {}
|
|
||||||
usage_frequency = {}
|
|
||||||
total_connections = 0
|
|
||||||
|
|
||||||
# Get date ranges for analysis
|
|
||||||
from django.utils import timezone
|
|
||||||
from datetime import timedelta
|
|
||||||
now = timezone.now()
|
|
||||||
recent_date = now - timedelta(days=30)
|
|
||||||
|
|
||||||
for acl_link in acl_links:
|
|
||||||
# Since we can't reliably track individual link usage from AccessLog.data,
|
|
||||||
# we'll count all successful connections to the server for this user
|
|
||||||
# and distribute them among all links for this server
|
|
||||||
|
|
||||||
# Get all successful connections for this user on this server
|
|
||||||
server_connections = AccessLog.objects.filter(
|
|
||||||
user=user.username,
|
|
||||||
server=acl_link.acl.server.name,
|
|
||||||
action='Success'
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Get recent connections (last 30 days)
|
|
||||||
server_recent_connections = AccessLog.objects.filter(
|
|
||||||
user=user.username,
|
|
||||||
server=acl_link.acl.server.name,
|
|
||||||
action='Success',
|
|
||||||
timestamp__gte=recent_date
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Get number of links for this server for this user
|
|
||||||
user_links_on_server = ACLLink.objects.filter(
|
|
||||||
acl__user=user,
|
|
||||||
acl__server=acl_link.acl.server
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Distribute connections evenly among links
|
|
||||||
link_connections = server_connections // user_links_on_server if user_links_on_server > 0 else 0
|
|
||||||
recent_connections = server_recent_connections // user_links_on_server if user_links_on_server > 0 else 0
|
|
||||||
|
|
||||||
# Calculate daily usage for the last 30 days
|
|
||||||
daily_usage = []
|
|
||||||
for i in range(30):
|
|
||||||
day_start = now - timedelta(days=i+1)
|
|
||||||
day_end = now - timedelta(days=i)
|
|
||||||
|
|
||||||
day_connections = AccessLog.objects.filter(
|
|
||||||
user=user.username,
|
|
||||||
server=acl_link.acl.server.name,
|
|
||||||
action='Success',
|
|
||||||
timestamp__gte=day_start,
|
|
||||||
timestamp__lt=day_end
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Distribute daily connections among links
|
|
||||||
day_link_connections = day_connections // user_links_on_server if user_links_on_server > 0 else 0
|
|
||||||
daily_usage.append(day_link_connections)
|
|
||||||
|
|
||||||
# Reverse to show oldest to newest
|
|
||||||
daily_usage.reverse()
|
|
||||||
|
|
||||||
connection_stats[acl_link.link] = link_connections
|
|
||||||
recent_connection_stats[acl_link.link] = recent_connections
|
|
||||||
usage_frequency[acl_link.link] = daily_usage
|
|
||||||
total_connections += link_connections
|
|
||||||
|
|
||||||
# Group links by server
|
# Group links by server
|
||||||
servers_data = {}
|
servers_data = {}
|
||||||
total_links = 0
|
total_links = 0
|
||||||
|
total_connections = 0 # Can be calculated from AccessLog if needed
|
||||||
|
|
||||||
for link in acl_links:
|
for link in acl_links:
|
||||||
server = link.acl.server
|
server = link.acl.server
|
||||||
@@ -119,34 +50,22 @@ def userPortal(request, user_hash):
|
|||||||
'total_connections': 0,
|
'total_connections': 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add link information with connection stats
|
# Add link information with simple last access from denormalized field
|
||||||
link_url = f"{EXTERNAL_ADDRESS}/ss/{link.link}#{server_name}"
|
link_url = f"{EXTERNAL_ADDRESS}/ss/{link.link}#{server_name}"
|
||||||
connection_count = connection_stats.get(link.link, 0)
|
|
||||||
recent_count = recent_connection_stats.get(link.link, 0)
|
|
||||||
daily_usage = usage_frequency.get(link.link, [0] * 30)
|
|
||||||
|
|
||||||
servers_data[server_name]['links'].append({
|
servers_data[server_name]['links'].append({
|
||||||
'link': link,
|
'link': link,
|
||||||
'url': link_url,
|
'url': link_url,
|
||||||
'comment': link.comment or 'Default',
|
'comment': link.comment or 'Default',
|
||||||
'connections': connection_count,
|
'last_access': link.last_access_time,
|
||||||
'recent_connections': recent_count,
|
|
||||||
'daily_usage': daily_usage,
|
|
||||||
'max_daily': max(daily_usage) if daily_usage else 0,
|
|
||||||
})
|
})
|
||||||
servers_data[server_name]['total_connections'] += connection_count
|
|
||||||
total_links += 1
|
total_links += 1
|
||||||
|
|
||||||
# Calculate total recent connections from all links
|
|
||||||
total_recent_connections = sum(recent_connection_stats.values())
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'user': user,
|
'user': user,
|
||||||
'servers_data': servers_data,
|
'servers_data': servers_data,
|
||||||
'total_servers': len(servers_data),
|
'total_servers': len(servers_data),
|
||||||
'total_links': total_links,
|
'total_links': total_links,
|
||||||
'total_connections': total_connections,
|
|
||||||
'recent_connections': total_recent_connections,
|
|
||||||
'external_address': EXTERNAL_ADDRESS,
|
'external_address': EXTERNAL_ADDRESS,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +103,7 @@ def userFrontend(request, user_hash):
|
|||||||
def shadowsocks(request, link):
|
def shadowsocks(request, link):
|
||||||
from .models import ACLLink, AccessLog
|
from .models import ACLLink, AccessLog
|
||||||
import logging
|
import logging
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -244,6 +164,11 @@ def shadowsocks(request, link):
|
|||||||
}
|
}
|
||||||
response = yaml.dump(config, allow_unicode=True)
|
response = yaml.dump(config, allow_unicode=True)
|
||||||
|
|
||||||
|
# Update last access time for this specific link
|
||||||
|
acl_link.last_access_time = timezone.now()
|
||||||
|
acl_link.save(update_fields=['last_access_time'])
|
||||||
|
|
||||||
|
# Still create AccessLog for audit purposes
|
||||||
AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Success", data=response)
|
AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Success", data=response)
|
||||||
|
|
||||||
return HttpResponse(response, content_type=f"{ 'application/json; charset=utf-8' if request.GET.get('mode') == 'json' else 'text/html' }")
|
return HttpResponse(response, content_type=f"{ 'application/json; charset=utf-8' if request.GET.get('mode') == 'json' else 'text/html' }")
|
||||||
|
Reference in New Issue
Block a user