Added access logs.

This commit is contained in:
A B
2024-10-28 17:15:49 +00:00
parent 7cf99af20d
commit 0880401cc4
7 changed files with 145 additions and 61 deletions

View File

@ -17,12 +17,11 @@ Including another URLconf
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.views.generic import RedirectView from django.views.generic import RedirectView
from vpn.views import shadowsocks, print_headers from vpn.views import shadowsocks
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('ss/<path:link>', shadowsocks, name='shadowsocks'), path('ss/<path:link>', shadowsocks, name='shadowsocks'),
path('dynamic/<path:link>', shadowsocks, name='shadowsocks'), path('dynamic/<path:link>', shadowsocks, name='shadowsocks'),
path('headers/', print_headers, name='print_headers'),
path('', RedirectView.as_view(url='/admin/', permanent=False)), path('', RedirectView.as_view(url='/admin/', permanent=False)),
] ]

View File

@ -7,9 +7,9 @@ from django.utils.safestring import mark_safe
from django.db.models import Count from django.db.models import Count
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from .models import User from .models import User, AccessLog
from django.utils.timezone import localtime
from vpn.models import User, ACL from vpn.models import User, ACL, ACLLink
from vpn.forms import UserForm from vpn.forms import UserForm
from .server_plugins import ( from .server_plugins import (
Server, Server,
@ -23,6 +23,43 @@ admin.site.site_title = "VPN Manager"
admin.site.site_header = "VPN Manager" admin.site.site_header = "VPN Manager"
admin.site.index_title = "OutFleet" admin.site.index_title = "OutFleet"
def format_object(data):
try:
if isinstance(data, dict):
formatted_data = json.dumps(data, indent=2)
return mark_safe(f"<pre>{formatted_data}</pre>")
elif isinstance(data, str):
return mark_safe(f"<pre>{data}</pre>")
else:
return mark_safe(f"<pre>{str(data)}</pre>")
except Exception as e:
return mark_safe(f"<span style='color: red;'>Error: {e}</span>")
class UserNameFilter(admin.SimpleListFilter):
title = 'User'
parameter_name = 'user'
def lookups(self, request, model_admin):
users = set(User.objects.values_list('username', flat=True))
return [(user, user) for user in users]
def queryset(self, request, queryset):
if self.value():
return queryset.filter(user__username=self.value())
return queryset
class ServerNameFilter(admin.SimpleListFilter):
title = 'Server Name'
parameter_name = 'acl__server__name'
def lookups(self, request, model_admin):
servers = set(ACL.objects.values_list('server__name', flat=True))
return [(server, server) for server in servers]
def queryset(self, request, queryset):
if self.value():
return queryset.filter(acl__server__name=self.value())
return queryset
@admin.register(Server) @admin.register(Server)
class ServerAdmin(PolymorphicParentModelAdmin): class ServerAdmin(PolymorphicParentModelAdmin):
@ -56,6 +93,7 @@ class ServerAdmin(PolymorphicParentModelAdmin):
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
form = UserForm form = UserForm
list_display = ('username', 'comment', 'registration_date', 'hash', 'server_count') list_display = ('username', 'comment', 'registration_date', 'hash', 'server_count')
list_editable = ('hash', )
search_fields = ('username', 'hash') search_fields = ('username', 'hash')
readonly_fields = ('hash',) readonly_fields = ('hash',)
@ -78,13 +116,40 @@ class UserAdmin(admin.ModelAdmin):
for server in selected_servers: for server in selected_servers:
ACL.objects.get_or_create(user=obj, server=server) ACL.objects.get_or_create(user=obj, server=server)
@admin.register(AccessLog)
class AccessLogAdmin(admin.ModelAdmin):
list_display = ('user', 'server', 'action', 'formatted_timestamp')
list_filter = (UserNameFilter, ServerNameFilter, 'timestamp')
search_fields = ('accesslog__user', 'server', 'timestamp')
readonly_fields = ('server', 'user', 'formatted_timestamp', 'action', 'formated_data')
@admin.display(description='Timestamp')
def formatted_timestamp(self, obj):
local_time = localtime(obj.timestamp)
return local_time.strftime('%Y-%m-%d %H:%M:%S %Z')
@admin.display(description='Details')
def formated_data(self, obj):
return format_object(obj.data)
class ACLLinkInline(admin.TabularInline):
model = ACLLink
extra = 1
help_text = 'Add or change ACL links'
verbose_name = 'Dynamic link'
verbose_name_plural = 'Dynamic links'
fields = ('link',)
@admin.register(ACL) @admin.register(ACL)
class ACLAdmin(admin.ModelAdmin): class ACLAdmin(admin.ModelAdmin):
list_display = ('user', 'server', 'server_type', 'link', 'created_at')
list_filter = ('user', 'server__server_type') list_display = ('user', 'server', 'server_type', 'display_links', 'created_at')
search_fields = ('user__name', 'server__name', 'server__comment', 'user__comment', 'link') list_editable = ('server', )
readonly_fields = ('user_info', ) list_filter = (UserNameFilter, 'server__server_type', ServerNameFilter)
search_fields = ('user__name', 'server__name', 'server__comment', 'user__comment', 'links__link')
readonly_fields = ('user_info',)
inlines = [ACLLinkInline]
@admin.display(description='Server Type', ordering='server__server_type') @admin.display(description='Server Type', ordering='server__server_type')
def server_type(self, obj): def server_type(self, obj):
@ -96,13 +161,12 @@ class ACLAdmin(admin.ModelAdmin):
user = obj.user user = obj.user
try: try:
data = server.get_user(user) data = server.get_user(user)
return format_object(data)
if isinstance(data, dict):
formatted_data = json.dumps(data, indent=2)
return mark_safe(f"<pre>{formatted_data}</pre>")
elif isinstance(data, str):
return mark_safe(f"<pre>{data}</pre>")
else:
return mark_safe(f"<pre>{str(data)}</pre>")
except Exception as e: except Exception as e:
return mark_safe(f"<span style='color: red;'>Error: {e}</span>") return mark_safe(f"<span style='color: red;'>Error: {e}</span>")
@admin.display(description='Links')
def display_links(self, obj):
links = obj.links.all()
return mark_safe('<br>'.join([link.link for link in links]))

View File

@ -8,14 +8,23 @@ import shortuuid
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
class AccessLog(models.Model):
user = models.CharField(max_length=256, blank=True, null=True, editable=False)
server = models.CharField(max_length=256, blank=True, null=True, editable=False)
action = models.CharField(max_length=100, editable=False)
data = models.TextField(default="", blank=True, editable=False)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.action} {self.user} request for {self.server} at {self.timestamp}"
class User(AbstractUser): class User(AbstractUser):
#is_active = False #is_active = False
comment = models.TextField(default="", blank=True) comment = models.TextField(default="", blank=True, help_text="Free form user comment")
registration_date = models.DateTimeField(auto_now_add=True) registration_date = models.DateTimeField(auto_now_add=True, verbose_name="Created")
servers = models.ManyToManyField('Server', through='ACL', blank=True) servers = models.ManyToManyField('Server', through='ACL', blank=True, help_text="Servers user has access to")
last_access = models.DateTimeField(null=True, blank=True) last_access = models.DateTimeField(null=True, blank=True)
hash = models.CharField(max_length=64, unique=True) hash = models.CharField(max_length=64, unique=True, help_text="Random user hash. It's using for client config generation.")
def get_servers(self): def get_servers(self):
return Server.objects.filter(acl__user=self) return Server.objects.filter(acl__user=self)
@ -32,22 +41,15 @@ class User(AbstractUser):
class ACL(models.Model): class ACL(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE) user = models.ForeignKey('User', on_delete=models.CASCADE)
server = models.ForeignKey('Server', on_delete=models.CASCADE) server = models.ForeignKey('Server', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created")
link = models.CharField(max_length=64, unique=True, blank=True, null=True)
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint(fields=['user', 'server'], name='unique_user_server') models.UniqueConstraint(fields=['user', 'server'], name='unique_user_server')
] ]
def __str__(self): def __str__(self):
return f"{self.user.username} - {self.server.name}" return f"{self.user.username} - {self.server.name}"
def save(self, *args, **kwargs):
if not self.link:
self.link = shortuuid.ShortUUID().random(length=16)
super().save(*args, **kwargs)
@receiver(post_save, sender=ACL) @receiver(post_save, sender=ACL)
def acl_created_or_updated(sender, instance, created, **kwargs): def acl_created_or_updated(sender, instance, created, **kwargs):
@ -55,4 +57,17 @@ def acl_created_or_updated(sender, instance, created, **kwargs):
@receiver(pre_delete, sender=ACL) @receiver(pre_delete, sender=ACL)
def acl_deleted(sender, instance, **kwargs): def acl_deleted(sender, instance, **kwargs):
sync_user.delay_on_commit(instance.user.id, instance.server.id) sync_user.delay_on_commit(instance.user.id, instance.server.id)
class ACLLink(models.Model):
acl = models.ForeignKey(ACL, related_name='links', on_delete=models.CASCADE)
link = models.CharField(max_length=64, unique=True, blank=True, null=True, verbose_name="Access link", help_text="Access link to get dynamic configuration")
def save(self, *args, **kwargs):
if not self.link:
self.link = shortuuid.ShortUUID().random(length=16)
super().save(*args, **kwargs)
def __str__(self):
return self.link

View File

@ -9,9 +9,9 @@ class Server(PolymorphicModel):
('Wireguard', 'Wireguard'), ('Wireguard', 'Wireguard'),
) )
name = models.CharField(max_length=100) name = models.CharField(max_length=100, help_text="Server name")
comment = models.TextField(default="", blank=True) comment = models.TextField(default="", blank=True)
registration_date = models.DateTimeField(auto_now_add=True) registration_date = models.DateTimeField(auto_now_add=True, verbose_name="Created")
server_type = models.CharField(max_length=50, choices=SERVER_TYPE_CHOICES, editable=False) server_type = models.CharField(max_length=50, choices=SERVER_TYPE_CHOICES, editable=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -37,11 +37,10 @@ class _FingerprintAdapter(requests.adapters.HTTPAdapter):
class OutlineServer(Server): class OutlineServer(Server):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
admin_url = models.URLField() admin_url = models.URLField(help_text="Management URL")
admin_access_cert = models.CharField(max_length=255) admin_access_cert = models.CharField(max_length=255, help_text="Fingerprint")
client_server_name = models.CharField(max_length=255) client_hostname = models.CharField(max_length=255, help_text="Server address for clients")
client_hostname = models.CharField(max_length=255) client_port = models.CharField(max_length=5, help_text="Server port for clients")
client_port = models.CharField(max_length=5)
class Meta: class Meta:
verbose_name = 'Outline' verbose_name = 'Outline'
@ -146,7 +145,7 @@ class OutlineServer(Server):
if server_user: if server_user:
if server_user.method != "chacha20-ietf-poly1305" or \ if server_user.method != "chacha20-ietf-poly1305" or \
server_user.port != int(self.client_port) or \ server_user.port != int(self.client_port) or \
server_user.username != user.username or \ server_user.name != user.username or \
server_user.password != user.hash or \ server_user.password != user.hash or \
self.client.delete_key(user.hash): self.client.delete_key(user.hash):
@ -213,19 +212,18 @@ class OutlineServer(Server):
class OutlineServerAdmin(PolymorphicChildModelAdmin): class OutlineServerAdmin(PolymorphicChildModelAdmin):
base_model = OutlineServer base_model = OutlineServer
show_in_index = False # Не отображать в главном списке админки show_in_index = False
list_display = ( list_display = (
'name', 'name',
'admin_url', 'admin_url',
'admin_access_cert', 'admin_access_cert',
'client_server_name',
'client_hostname', 'client_hostname',
'client_port', 'client_port',
'server_status_inline',
'user_count', 'user_count',
'registration_date' 'server_status_inline',
) )
readonly_fields = ('server_status_full', ) readonly_fields = ('server_status_full', 'registration_date',)
list_editable = ('admin_url', 'admin_access_cert', 'client_hostname', 'client_port',)
exclude = ('server_type',) exclude = ('server_type',)
@admin.display(description='Clients', ordering='user_count') @admin.display(description='Clients', ordering='user_count')

View File

@ -29,11 +29,13 @@ def sync_all_users():
@shared_task(name="sync_all_users_on_server") @shared_task(name="sync_all_users_on_server")
def sync_users(server_id): def sync_users(server_id):
from .models import Server from .models import Server
status = {}
try: try:
server = Server.objects.get(id=server_id) server = Server.objects.get(id=server_id)
server.sync_users() sync = server.sync_users()
logger.info(f"Successfully synced users for server {server.name}") if sync:
logger.info(f"Successfully synced users for server {server.name}")
return f"Successfully synced users for server {server.name}"
except Exception as e: except Exception as e:
logger.error(f"Error syncing users for server {server.name}: {e}") logger.error(f"Error syncing users for server {server.name}: {e}")
raise TaskFailedException(message=f"Error syncing users for server {server.name}") raise TaskFailedException(message=f"Error syncing users for server {server.name}")

View File

@ -1,21 +1,21 @@
import json
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.http import JsonResponse from django.http import JsonResponse, HttpResponse, Http404
from django.http import HttpResponse
def print_headers(request):
headers = {key: value for key, value in request.META.items() if key.startswith('HTTP_')}
for key, value in headers.items():
print(f'{key}: {value}')
return HttpResponse(f"Headers: {headers}")
def shadowsocks(request, link): def shadowsocks(request, link):
from .models import ACL from .models import ACLLink, AccessLog
acl = get_object_or_404(ACL, link=link) try:
acl_link = get_object_or_404(ACLLink, link=link)
acl = acl_link.acl
except Http404:
AccessLog.objects.create(user=None, server="Unknown", action="Failed", data=f"ACL not found for link: {link}")
return JsonResponse({"error": "Not allowed"}, status=403)
try: try:
server_user = acl.server.get_user(acl.user, raw=True) server_user = acl.server.get_user(acl.user, raw=True)
except Exception as e: except Exception as e:
AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Failed", data=f"{e}")
return JsonResponse({"error": f"Couldn't get credentials from server. {e}"}) return JsonResponse({"error": f"Couldn't get credentials from server. {e}"})
config = { config = {
@ -26,7 +26,13 @@ def shadowsocks(request, link):
"server": acl.server.client_hostname, "server": acl.server.client_hostname,
"server_port": server_user.port, "server_port": server_user.port,
"access_url": server_user.access_url, "access_url": server_user.access_url,
"outfleet": {
"acl_link": link,
"server_name": acl.server.name,
"server_type": acl.server.server_type,
}
} }
return JsonResponse(config)
AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Success", data=json.dumps(config, indent=2))
return JsonResponse(config)