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.urls import path, include
from django.views.generic import RedirectView
from vpn.views import shadowsocks, print_headers
from vpn.views import shadowsocks
urlpatterns = [
path('admin/', admin.site.urls),
path('ss/<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)),
]

View File

@ -7,9 +7,9 @@ from django.utils.safestring import mark_safe
from django.db.models import Count
from django.contrib.auth.admin import UserAdmin
from .models import User
from vpn.models import User, ACL
from .models import User, AccessLog
from django.utils.timezone import localtime
from vpn.models import User, ACL, ACLLink
from vpn.forms import UserForm
from .server_plugins import (
Server,
@ -23,6 +23,43 @@ admin.site.site_title = "VPN Manager"
admin.site.site_header = "VPN Manager"
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)
class ServerAdmin(PolymorphicParentModelAdmin):
@ -56,6 +93,7 @@ class ServerAdmin(PolymorphicParentModelAdmin):
class UserAdmin(admin.ModelAdmin):
form = UserForm
list_display = ('username', 'comment', 'registration_date', 'hash', 'server_count')
list_editable = ('hash', )
search_fields = ('username', 'hash')
readonly_fields = ('hash',)
@ -78,13 +116,40 @@ class UserAdmin(admin.ModelAdmin):
for server in selected_servers:
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)
class ACLAdmin(admin.ModelAdmin):
list_display = ('user', 'server', 'server_type', 'link', 'created_at')
list_filter = ('user', 'server__server_type')
search_fields = ('user__name', 'server__name', 'server__comment', 'user__comment', 'link')
readonly_fields = ('user_info', )
list_display = ('user', 'server', 'server_type', 'display_links', 'created_at')
list_editable = ('server', )
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')
def server_type(self, obj):
@ -96,13 +161,12 @@ class ACLAdmin(admin.ModelAdmin):
user = obj.user
try:
data = server.get_user(user)
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>")
return format_object(data)
except Exception as e:
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
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):
#is_active = False
comment = models.TextField(default="", blank=True)
registration_date = models.DateTimeField(auto_now_add=True)
servers = models.ManyToManyField('Server', through='ACL', blank=True)
comment = models.TextField(default="", blank=True, help_text="Free form user comment")
registration_date = models.DateTimeField(auto_now_add=True, verbose_name="Created")
servers = models.ManyToManyField('Server', through='ACL', blank=True, help_text="Servers user has access to")
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):
return Server.objects.filter(acl__user=self)
@ -32,23 +41,16 @@ class User(AbstractUser):
class ACL(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE)
server = models.ForeignKey('Server', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
link = models.CharField(max_length=64, unique=True, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created")
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'server'], name='unique_user_server')
]
def __str__(self):
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)
def acl_created_or_updated(sender, instance, created, **kwargs):
sync_user.delay_on_commit(instance.user.id, instance.server.id)
@ -56,3 +58,16 @@ def acl_created_or_updated(sender, instance, created, **kwargs):
@receiver(pre_delete, sender=ACL)
def acl_deleted(sender, instance, **kwargs):
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'),
)
name = models.CharField(max_length=100)
name = models.CharField(max_length=100, help_text="Server name")
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)
def __init__(self, *args, **kwargs):

View File

@ -37,11 +37,10 @@ class _FingerprintAdapter(requests.adapters.HTTPAdapter):
class OutlineServer(Server):
logger = logging.getLogger(__name__)
admin_url = models.URLField()
admin_access_cert = models.CharField(max_length=255)
client_server_name = models.CharField(max_length=255)
client_hostname = models.CharField(max_length=255)
client_port = models.CharField(max_length=5)
admin_url = models.URLField(help_text="Management URL")
admin_access_cert = models.CharField(max_length=255, help_text="Fingerprint")
client_hostname = models.CharField(max_length=255, help_text="Server address for clients")
client_port = models.CharField(max_length=5, help_text="Server port for clients")
class Meta:
verbose_name = 'Outline'
@ -146,7 +145,7 @@ class OutlineServer(Server):
if server_user:
if server_user.method != "chacha20-ietf-poly1305" 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 \
self.client.delete_key(user.hash):
@ -213,19 +212,18 @@ class OutlineServer(Server):
class OutlineServerAdmin(PolymorphicChildModelAdmin):
base_model = OutlineServer
show_in_index = False # Не отображать в главном списке админки
show_in_index = False
list_display = (
'name',
'admin_url',
'admin_access_cert',
'client_server_name',
'client_hostname',
'client_port',
'server_status_inline',
'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',)
@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")
def sync_users(server_id):
from .models import Server
status = {}
try:
server = Server.objects.get(id=server_id)
server.sync_users()
logger.info(f"Successfully synced users for server {server.name}")
sync = server.sync_users()
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:
logger.error(f"Error syncing users for server {server.name}: {e}")
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.http import JsonResponse
from django.http import HttpResponse
from django.http import JsonResponse, HttpResponse, Http404
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):
from .models import ACL
acl = get_object_or_404(ACL, link=link)
from .models import ACLLink, AccessLog
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:
server_user = acl.server.get_user(acl.user, raw=True)
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}"})
config = {
@ -26,7 +26,13 @@ def shadowsocks(request, link):
"server": acl.server.client_hostname,
"server_port": server_user.port,
"access_url": server_user.access_url,
"outfleet": {
"acl_link": link,
"server_name": acl.server.name,
"server_type": acl.server.server_type,
}
}
AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Success", data=json.dumps(config, indent=2))
return JsonResponse(config)