From 47caca84681de7d72be0b425cd61894aa976ae2d Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Thu, 4 Apr 2024 16:23:59 +0200 Subject: [PATCH] #1073 - wip --- config/api_router.py | 25 -- config/settings/base.py | 14 - config/urls.py | 34 +-- examples/local_api.ipynb | 65 +++-- orochi/api/api.py | 16 ++ orochi/api/filters.py | 18 ++ orochi/api/models.py | 212 ++++++++++++++ orochi/api/permissions.py | 33 +++ orochi/api/routers/auth.py | 87 ++++++ orochi/api/routers/dumps.py | 50 ++++ orochi/api/routers/folders.py | 43 +++ orochi/api/routers/plugins.py | 42 +++ orochi/api/routers/users.py | 45 +++ orochi/api/routers/utils.py | 23 ++ orochi/templates/base.html | 2 +- orochi/templates/rest_framework/api.html | 6 - orochi/templates/users/user_plugins.html | 14 +- orochi/templates/website/index.html | 13 +- orochi/templates/website/partial_folder.html | 2 +- orochi/users/api/serializers.py | 30 -- orochi/users/api/views.py | 59 ---- orochi/users/tests/test_drf_urls.py | 24 -- orochi/users/tests/test_drf_views.py | 104 ------- orochi/website/api/permissions.py | 58 ---- orochi/website/api/serializers.py | 164 ----------- orochi/website/api/views.py | 277 ------------------- orochi/website/urls.py | 4 - orochi/website/views.py | 75 +---- requirements/base.txt | 10 +- 29 files changed, 660 insertions(+), 889 deletions(-) delete mode 100644 config/api_router.py create mode 100644 orochi/api/api.py create mode 100644 orochi/api/filters.py create mode 100644 orochi/api/models.py create mode 100644 orochi/api/permissions.py create mode 100644 orochi/api/routers/auth.py create mode 100644 orochi/api/routers/dumps.py create mode 100644 orochi/api/routers/folders.py create mode 100644 orochi/api/routers/plugins.py create mode 100644 orochi/api/routers/users.py create mode 100644 orochi/api/routers/utils.py delete mode 100644 orochi/templates/rest_framework/api.html delete mode 100644 orochi/users/api/serializers.py delete mode 100644 orochi/users/api/views.py delete mode 100644 orochi/users/tests/test_drf_urls.py delete mode 100644 orochi/users/tests/test_drf_views.py delete mode 100644 orochi/website/api/permissions.py delete mode 100644 orochi/website/api/serializers.py delete mode 100644 orochi/website/api/views.py diff --git a/config/api_router.py b/config/api_router.py deleted file mode 100644 index c3ea5dd4..00000000 --- a/config/api_router.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.conf import settings -from django.urls import include, path -from rest_framework_nested import routers - -from orochi.users.api.views import UserViewSet -from orochi.website.api.views import DumpViewSet, PluginViewSet, ResultViewSet - -if settings.DEBUG: - router = routers.DefaultRouter() -else: - router = routers.SimpleRouter() - -router.register(r"users", UserViewSet) -router.register(r"dumps", DumpViewSet) -router.register(r"plugin", PluginViewSet) -dumps_router = routers.NestedSimpleRouter(router, r"dumps", lookup="dump") -dumps_router.register(r"results", ResultViewSet, basename="dump-plugins") -extdumps_router = routers.NestedSimpleRouter(dumps_router, r"results", lookup="result") - -app_name = "api" -urlpatterns = [ - path(r"", include(router.urls)), - path(r"", include(dumps_router.urls)), - path(r"", include(extdumps_router.urls)), -] diff --git a/config/settings/base.py b/config/settings/base.py index 0ae5f334..6bbc26c4 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -68,9 +68,6 @@ "guardian", "widget_tweaks", "django_json_widget", - "rest_framework", - "rest_framework.authtoken", - "drf_yasg", "django_admin_listfilter_dropdown", "django_admin_multiple_choice_list_filter", ] @@ -296,17 +293,6 @@ ) AUTH_LDAP_USER_ATTR_MAP = env.dict("AUTH_LDAP_USER_ATTR_MAP") -# REST FRAMEWORK -# ------------------------------------------------------------------------------- -REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework.authentication.SessionAuthentication", - "rest_framework.authentication.TokenAuthentication", - ), - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), - "TEST_REQUEST_DEFAULT_FORMAT": "json", -} - # django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup CORS_URLS_REGEX = r"^/api/.*$" CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS") diff --git a/config/urls.py b/config/urls.py index 4ff24648..2abd95d2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -2,12 +2,10 @@ from django.conf.urls.static import static from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.urls import include, path, re_path +from django.urls import include, path from django.views import defaults as default_views -from drf_yasg import openapi -from drf_yasg.views import get_schema_view -from rest_framework import permissions -from rest_framework.authtoken.views import obtain_auth_token + +from orochi.api.api import api # DJANGO VIEWS urlpatterns = [ @@ -22,31 +20,9 @@ urlpatterns += staticfiles_urlpatterns() # API URLS -urlpatterns += [ - path("api/", include("config.api_router")), - path("auth-token/", obtain_auth_token), -] - -# SWAGGER -schema_view = get_schema_view( - openapi.Info(title="Orochi API", default_version="v1"), - public=True, - permission_classes=(permissions.AllowAny,), -) -urlpatterns += [ - re_path( - r"^swagger(?P\.json)$", - schema_view.without_ui(cache_timeout=0), - name="schema-json", - ), - path( - r"swagger/", - schema_view.with_ui("swagger", cache_timeout=0), - name="schema-swagger-ui", - ), - path(r"redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), -] +urlpatterns += [path("api/", api.urls)] +# DEBUG if settings.DEBUG: urlpatterns += [ path( diff --git a/examples/local_api.ipynb b/examples/local_api.ipynb index 2426adaa..ffa29574 100644 --- a/examples/local_api.ipynb +++ b/examples/local_api.ipynb @@ -6,11 +6,14 @@ "metadata": {}, "outputs": [], "source": [ + "import json\n", "import getpass\n", "from requests import Session\n", "from pprint import pprint\n", + "import urllib3\n", + "urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n", "\n", - "url = \"http://127.0.0.1\"\n", + "url = \"https://localhost\"\n", "user = input()\n", "password = getpass.getpass()" ] @@ -29,15 +32,26 @@ "outputs": [], "source": [ "session = Session()\n", + "\n", + "first = session.get(f\"{url}\", verify=False)\n", + "csrftoken = first.cookies[\"csrftoken\"]\n", + "\n", + "data = json.dumps(\n", + " {\"username\": user, \"password\": password, \"csrfmiddlewaretoken\": csrftoken}\n", + ")\n", + "\n", + "headers = {\n", + " \"X-CSRFToken\": first.headers[\"Set-Cookie\"].split(\"=\")[1].split(\";\")[0],\n", + " \"Referer\": url,\n", + " \"X-Requested-With\": \"XMLHttpRequest\",\n", + "}\n", + "\n", "req = session.post(\n", - " f\"{url}/auth-token/\", \n", - " data={\"username\": user, \"password\": password}\n", + " f\"{url}/api/auth/\", data=data, cookies=first.cookies, verify=False, headers=headers\n", ")\n", "if req.status_code != 200:\n", - " print(req.json())\n", - " exit(1)\n", - "token = req.json()[\"token\"]\n", - "session.headers[\"Authorization\"] = f\"Token {token}\"" + " print(req.text)\n", + " exit(1)" ] }, { @@ -71,6 +85,7 @@ "metadata": {}, "outputs": [], "source": [ + "\"\"\" TODO\n", "files = {'upload': open('/home/DATA/AMF_MemorySamples/linux/sorpresa.zip','rb')}\n", "values = {'operating_system': 'Linux', 'name': 'sorpresa'}\n", "res = session.post(f\"{url}/api/dumps/\", files=files, data=values)\n", @@ -78,7 +93,8 @@ " pprint(res.json())\n", " dump_pk = res.json()[\"pk\"]\n", "else:\n", - " print(res.status_code)" + " print(res.status_code)\n", + "\"\"\"" ] }, { @@ -94,6 +110,7 @@ "metadata": {}, "outputs": [], "source": [ + "\"\"\" TODO\n", "# This code requires a file on the server in the folder specified in the LOCAL_UPLOAD_PATH\n", "# settings folder\n", "\n", @@ -105,7 +122,8 @@ "if res.status_code == 200:\n", " pprint(res.json())\n", "else:\n", - " print(res.status_code)" + " print(res.status_code)\n", + "\"\"\"" ] }, { @@ -121,7 +139,12 @@ "metadata": {}, "outputs": [], "source": [ - "res = session.get(f\"{url}/api/plugin/\")\n", + "res = session.get(f\"{url}/api/plugins/\")\n", + "if res.status_code == 200:\n", + " plugins = res.json()\n", + " print(f\"{len(plugins)} plugins found\")\n", + " pprint(plugins[0])\n", + "res = session.get(f\"{url}/api/plugins/?operating_system=Other\")\n", "if res.status_code == 200:\n", " plugins = res.json()\n", " print(f\"{len(plugins)} plugins found\")\n", @@ -141,11 +164,13 @@ "metadata": {}, "outputs": [], "source": [ + "\"\"\" TODO\n", "res = session.get(f\"{url}/api/dumps/{dump_pk}/results/\")\n", "if res.status_code == 200:\n", " pprint(res.json())\n", " result_pk = [x['pk'] for x in res.json() if x['plugin'] == 'linux.pslist.PsList'][0]\n", - " print(res.status_code)" + " print(res.status_code)\n", + "\"\"\"" ] }, { @@ -161,11 +186,13 @@ "metadata": {}, "outputs": [], "source": [ + "\"\"\" TODO\n", "res = session.post(f\"{url}/api/dumps/{dump_pk}/results/{result_pk}/resubmit/\", data={'parameter': {'dump': True}})\n", "if res.status_code == 200:\n", " pprint(res.json())\n", "else:\n", - " print(res.status_code)" + " print(res.status_code)\n", + "\"\"\"" ] }, { @@ -181,6 +208,7 @@ "metadata": {}, "outputs": [], "source": [ + "\"\"\" TODO\n", "status = 'Running'\n", "while status != 'Success':\n", " res = session.get(f\"{url}/api/dumps/{dump_pk}/results/{result_pk}/\")\n", @@ -189,7 +217,8 @@ " pprint(status)\n", " else:\n", " print(res.status_code)\n", - " break" + " break\n", + "\"\"\"" ] }, { @@ -205,11 +234,13 @@ "metadata": {}, "outputs": [], "source": [ + "\"\"\" TODO\n", "res = session.get(f\"{url}/api/dumps/{dump_pk}/results/{result_pk}/result\")\n", "if res.status_code == 200:\n", " pprint(len(res.json()))\n", "else:\n", - " print(res.status_code)" + " print(res.status_code)\n", + "\"\"\"" ] }, { @@ -225,10 +256,12 @@ "metadata": {}, "outputs": [], "source": [ + "\"\"\" TODO\n", "import pandas as pd\n", "import pygwalker as pyg\n", "df = pd.DataFrame.from_records(res.json())\n", - "walker = pyg.walk(df)" + "walker = pyg.walk(df)\n", + "\"\"\"" ] } ], @@ -248,7 +281,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/orochi/api/api.py b/orochi/api/api.py new file mode 100644 index 00000000..c64dd814 --- /dev/null +++ b/orochi/api/api.py @@ -0,0 +1,16 @@ +from ninja import NinjaAPI + +from orochi.api.routers.auth import router as auth_router +from orochi.api.routers.dumps import router as dumps_router +from orochi.api.routers.folders import router as folders_router +from orochi.api.routers.plugins import router as plugins_router +from orochi.api.routers.users import router as users_router +from orochi.api.routers.utils import router as utils_router + +api = NinjaAPI(csrf=True, title="Orochi API", urls_namespace="api") +api.add_router("/auth/", auth_router, tags=["Auth"]) +api.add_router("/users/", users_router, tags=["Users"]) +api.add_router("/folders/", folders_router, tags=["Folders"]) +api.add_router("/dumps/", dumps_router, tags=["Dumps"]) +api.add_router("/plugins/", plugins_router, tags=["Plugins"]) +api.add_router("/utils/", utils_router, tags=["Utils"]) diff --git a/orochi/api/filters.py b/orochi/api/filters.py new file mode 100644 index 00000000..9dbadb37 --- /dev/null +++ b/orochi/api/filters.py @@ -0,0 +1,18 @@ +from enum import Enum + +from ninja import Schema + + +class OPERATING_SYSTEM(str, Enum): + WINDOWS = "Windows" + LINUX = "Linux" + MAC = "Mac" + OTHER = "Other" + + +class OperatingSytemFilters(Schema): + operating_system: OPERATING_SYSTEM = None + + +class DumpFilters(Schema): + result: int = None diff --git a/orochi/api/models.py b/orochi/api/models.py new file mode 100644 index 00000000..fe2fe886 --- /dev/null +++ b/orochi/api/models.py @@ -0,0 +1,212 @@ +from datetime import datetime +from typing import Dict, List, Optional + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from ninja import ModelSchema, Schema +from ninja.orm import create_schema + +from orochi.website.models import Dump, Folder, Plugin, Result + +################################################### +# Auth +################################################### +UsernameSchemaMixin = create_schema( + get_user_model(), fields=[get_user_model().USERNAME_FIELD] +) + +EmailSchemaMixin = create_schema( + get_user_model(), fields=[get_user_model().EMAIL_FIELD] +) + + +class LoginIn(UsernameSchemaMixin): + password: str + + +class RequestPasswordResetIn(EmailSchemaMixin): + pass + + +class SetPasswordIn(UsernameSchemaMixin): + new_password1: str + new_password2: str + token: str + + +class ChangePasswordIn(Schema): + old_password: str + new_password1: str + new_password2: str + + +################################################### +# General +################################################### +class ErrorsOut(Schema): + errors: str | List[str] | Dict[str, str | List[str]] + + +class DaskStatusOut(Schema): + running: int = 0 + + +################################################### +# Users +################################################### +class GroupSchema(ModelSchema): + class Meta: + model = Group + fields = ["id", "name"] + + +class UserOutSchema(ModelSchema): + groups: List[GroupSchema] = [] + + class Meta: + model = get_user_model() + fields = ["id", "username", "first_name", "last_name"] + + +class UserInSchema(ModelSchema): + class Meta: + model = get_user_model() + fields = [ + "username", + "email", + "first_name", + "last_name", + "password", + ] + + +################################################### +# Plugins +################################################### +class PluginOutSchema(ModelSchema): + + class Meta: + model = Plugin + fields = [ + "name", + "operating_system", + "disabled", + "local_dump", + "vt_check", + "clamav_check", + "regipy_check", + "yara_check", + "maxmind_check", + "local", + "local_date", + ] + + +class PluginInSchema(ModelSchema): + + class Meta: + model = Plugin + fields = [ + "operating_system", + "disabled", + "local_dump", + "vt_check", + "clamav_check", + "regipy_check", + "yara_check", + "maxmind_check", + "local", + "local_date", + ] + + +################################################### +# Folder +################################################### +class FolderSchema(ModelSchema): + class Meta: + model = Folder + fields = ["name"] + + +class FolderFullSchema(ModelSchema): + user: UserOutSchema = None + + class Meta: + model = Folder + fields = ["name"] + + +################################################### +# Dump +################################################### +class DumpSchema(ModelSchema): + + folder: Optional[FolderSchema] = None + + class Meta: + model = Dump + fields = [ + "index", + "name", + "color", + "operating_system", + "author", + "upload", + "status", + "description", + ] + + +class RegipyPluginSchema(Schema): + plugin: str = None + hive: str = None + data: dict | List[dict] = None + + +class DumpInfoSchema(ModelSchema): + folder: Optional[FolderSchema] = None + regipy_plugins: Optional[List[RegipyPluginSchema]] = None + suggested_symbols_path: Optional[List[str]] = None + author: UserOutSchema = None + + class Meta: + model = Dump + fields = [ + "index", + "name", + "comment", + "description", + "color", + "operating_system", + "md5", + "sha256", + "size", + "upload", + "banner", + ] + + +################################################### +# Result +################################################### +class PluginSmallSchema(ModelSchema): + class Meta: + model = Plugin + fields = ["name", "comment"] + + +class DumpSmallSchema(ModelSchema): + class Meta: + model = Dump + fields = ["index", "name"] + + +class ResultSmallOutSchema(ModelSchema): + plugin: PluginSmallSchema = None + dump: DumpSmallSchema = None + updated_at: datetime = None + + class Meta: + model = Result + fields = ["result", "parameter", "description"] diff --git a/orochi/api/permissions.py b/orochi/api/permissions.py new file mode 100644 index 00000000..95e7aa69 --- /dev/null +++ b/orochi/api/permissions.py @@ -0,0 +1,33 @@ +from functools import wraps + +from ninja.errors import HttpError + + +def ninja_permission_required(perm): + def decorator(func): + @wraps(func) + def wrapper(request, *args, **kwargs): + if request.user.has_perm(perm) is False: + raise HttpError(status_code=403, message="Permission Denied") + + return func(request, *args, **kwargs) + + return wrapper + + return decorator + + +def ninja_test_required(test): + def decorator(func): + @wraps(func) + def wrapper(request, *args, **kwargs): + if ( + test == "is_not_readonly" + and request.user.groups.filter(name="ReadOnly").exists() + ): + raise HttpError(status_code=403, message="Permission Denied") + return func(request, *args, **kwargs) + + return wrapper + + return decorator diff --git a/orochi/api/routers/auth.py b/orochi/api/routers/auth.py new file mode 100644 index 00000000..83754f3c --- /dev/null +++ b/orochi/api/routers/auth.py @@ -0,0 +1,87 @@ +from django.conf import settings +from django.contrib.auth import authenticate, get_user_model +from django.contrib.auth import login as django_login +from django.contrib.auth import logout as django_logout +from django.contrib.auth import update_session_auth_hash +from django.contrib.auth.forms import ( + PasswordChangeForm, + PasswordResetForm, + SetPasswordForm, +) +from django.contrib.auth.tokens import default_token_generator +from ninja import Router +from ninja.security import django_auth + +from orochi.api.models import ( + ChangePasswordIn, + ErrorsOut, + LoginIn, + RequestPasswordResetIn, + SetPasswordIn, + UserOutSchema, +) + +router = Router() +_LOGIN_BACKEND = "django.contrib.auth.backends.ModelBackend" + + +@router.post("/", response={200: UserOutSchema, 403: None}, auth=None) +def login(request, data: LoginIn): + user = authenticate(backend=_LOGIN_BACKEND, **data.dict()) + if user is not None and user.is_active: + django_login(request, user, backend=_LOGIN_BACKEND) + return user + return 403, None + + +@router.delete("/", response={204: None}, auth=django_auth) +def logout(request): + django_logout(request) + return 204, None + + +@router.post("/request_password_reset", response={204: None}, auth=None) +def request_password_reset(request, data: RequestPasswordResetIn): + form = PasswordResetForm(data.dict()) + if form.is_valid(): + form.save( + request=request, + extra_email_context=( + {"frontend_url": settings.FRONTEND_URL} + if hasattr(settings, "FRONTEND_URL") + else None + ), + ) + return 204, None + + +@router.post( + "/reset_password", + response={200: UserOutSchema, 403: ErrorsOut, 422: None}, + auth=None, +) +def reset_password(request, data: SetPasswordIn): + user_field = get_user_model().USERNAME_FIELD + user_data = {user_field: getattr(data, user_field)} + user = get_user_model().objects.filter(**user_data) + + if user.exists(): + user = user.get() + if default_token_generator.check_token(user, data.token): + form = SetPasswordForm(user, data.dict()) + if form.is_valid(): + form.save() + django_login(request, user, backend=_LOGIN_BACKEND) + return user + return 403, {"errors": dict(form.errors)} + return 422, None + + +@router.post("/change_password", response={200: None, 403: ErrorsOut}, auth=django_auth) +def change_password(request, data: ChangePasswordIn): + form = PasswordChangeForm(request.user, data.dict()) + if form.is_valid(): + form.save() + update_session_auth_hash(request, request.user) + return 200 + return 403, {"errors": dict(form.errors)} diff --git a/orochi/api/routers/dumps.py b/orochi/api/routers/dumps.py new file mode 100644 index 00000000..affd8f04 --- /dev/null +++ b/orochi/api/routers/dumps.py @@ -0,0 +1,50 @@ +from typing import List +from uuid import UUID + +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from guardian.shortcuts import get_objects_for_user +from ninja import Query, Router +from ninja.security import django_auth + +from orochi.api.filters import DumpFilters, OperatingSytemFilters +from orochi.api.models import DumpInfoSchema, DumpSchema, ResultSmallOutSchema +from orochi.website.models import Dump, Result + +router = Router() + + +@router.get("/", auth=django_auth, response=List[DumpSchema]) +def list_dumps(request, filters: Query[OperatingSytemFilters]): + dumps = ( + Dump.objects.all() + if request.user.is_superuser + else get_objects_for_user(request.user, "website.can_see") + ) + if filters and filters.operating_system: + dumps = [x for x in dumps if x.operating_system == filters.operating_system] + return dumps + + +@router.get("/{pk}", response=DumpInfoSchema, auth=django_auth) +def get_dump_info(request, pk: UUID): + dump = get_object_or_404(Dump, index=pk) + if dump not in get_objects_for_user(request.user, "website.can_see"): + return HttpResponse("Forbidden", status=403) + return dump + + +@router.get("/{idxs:pks}/plugins", response=ResultSmallOutSchema, auth=django_auth) +def get_dump_plugins(request, pks: List[UUID], filters: Query[DumpFilters] = None): + dumps_ok = get_objects_for_user(request.user, "website.can_see") + dumps = [ + dump.index for dump in Dump.objects.filter(index__in=pks) if dump in dumps_ok + ] + res = ( + Result.objects.select_related("dump", "plugin") + .filter(dump__index__in=dumps) + .order_by("plugin__name") + ) + if filters and filters.result: + res = res.filter(result=filters.result) + return res diff --git a/orochi/api/routers/folders.py b/orochi/api/routers/folders.py new file mode 100644 index 00000000..aaef6d25 --- /dev/null +++ b/orochi/api/routers/folders.py @@ -0,0 +1,43 @@ +from typing import List + +import django +import psycopg2 +from django.shortcuts import get_object_or_404 +from ninja import Router +from ninja.security import django_auth + +from orochi.api.models import ErrorsOut, FolderFullSchema, FolderSchema +from orochi.api.permissions import ninja_test_required +from orochi.website.models import Folder + +router = Router() + + +@router.get("/", auth=django_auth, response=List[FolderFullSchema]) +def list_folders(request): + if request.user.is_superuser: + return Folder.objects.all() + return Folder.objects.filter(user=request.user) + + +@router.post( + "/", + response={201: FolderFullSchema, 400: ErrorsOut}, + auth=django_auth, + url_name="folder_create", +) +@ninja_test_required("is_not_readonly") +def create_folder(request, folder_in: FolderSchema): + try: + folder = Folder.objects.create(name=folder_in.name, user=request.user) + folder.save() + return 201, folder + except (psycopg2.errors.UniqueViolation, django.db.utils.IntegrityError): + return 400, {"errors": "Folder already exists"} + + +@router.delete("/{name}", auth=django_auth, response={200: None}) +def delete_folder(request, name: str): + folder = get_object_or_404(Folder, name=name, user=request.user) + folder.delete() + return 200 diff --git a/orochi/api/routers/plugins.py b/orochi/api/routers/plugins.py new file mode 100644 index 00000000..f1f092b4 --- /dev/null +++ b/orochi/api/routers/plugins.py @@ -0,0 +1,42 @@ +from typing import List + +from django.shortcuts import get_object_or_404 +from ninja import Query, Router +from ninja.security import django_auth + +from orochi.api.filters import OperatingSytemFilters +from orochi.api.models import PluginInSchema, PluginOutSchema +from orochi.website.models import Plugin, UserPlugin + +router = Router() + + +@router.get("/", response={200: List[PluginOutSchema]}, auth=django_auth) +def list_plugins(request, filters: Query[OperatingSytemFilters] = None): + if filters and filters.operating_system: + return Plugin.objects.filter(operating_system=filters.operating_system) + return Plugin.objects.all() + + +@router.get("/{name}", response={200: PluginOutSchema}, auth=django_auth) +def get_plugin(request, name: str): + return Plugin.objects.get(name=name) + + +@router.put("/{name}", response={200: PluginOutSchema}, auth=django_auth) +def update_plugin(request, name: str, data: PluginInSchema): + plugin = get_object_or_404(Plugin, name=name) + for attr, value in data.dict().items(): + setattr(plugin, attr, value) + plugin.save() + return plugin + + +@router.post( + "/{name}/enable/{enable}", auth=django_auth, url_name="enable", response={200: None} +) +def enable_plugin(request, name: str, enable: bool): + plugin = get_object_or_404(UserPlugin, plugin__name=name, user=request.user) + plugin.automatic = enable + plugin.save() + return 200 diff --git a/orochi/api/routers/users.py b/orochi/api/routers/users.py new file mode 100644 index 00000000..5b724f61 --- /dev/null +++ b/orochi/api/routers/users.py @@ -0,0 +1,45 @@ +from typing import List + +from allauth.account.models import EmailAddress +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.shortcuts import get_object_or_404 +from ninja import Router +from ninja.pagination import paginate +from ninja.security import django_auth, django_auth_superuser + +from orochi.api.models import ErrorsOut, UserInSchema, UserOutSchema + +router = Router() + + +@router.post("/", response={201: UserOutSchema}, auth=django_auth_superuser) +def create_user(request, user_in: UserInSchema, is_readonly: bool = False): + user = get_user_model().objects.create_user(**user_in.dict()) + email, _ = EmailAddress.objects.get_or_create(user=user, email=user.email) + email.verified = True + email.save() + if is_readonly: + readonly_group = Group.objects.get(name="ReadOnly") + user.groups.add(readonly_group) + return 201, user + + +@router.get("/", response={200: List[UserOutSchema]}, auth=django_auth) +@paginate +def list_users(request): + return get_user_model().objects.all() + + +@router.get("/me", response={200: UserOutSchema, 403: ErrorsOut}) +def me(request): + if not request.user.is_authenticated: + return 403, {"errors": "Please sign in first"} + return request.user + + +@router.delete("/{username}", auth=django_auth_superuser, response={200: None}) +def delete_user(request, username: str): + user = get_object_or_404(get_user_model(), username=username) + user.delete() + return 200 diff --git a/orochi/api/routers/utils.py b/orochi/api/routers/utils.py new file mode 100644 index 00000000..2a002075 --- /dev/null +++ b/orochi/api/routers/utils.py @@ -0,0 +1,23 @@ +from dask.distributed import Client +from django.conf import settings +from ninja import Router +from ninja.security import django_auth + +from orochi.api.models import DaskStatusOut + +router = Router() + + +@router.get( + "/dask_status", auth=django_auth, response=DaskStatusOut, url_name="dask_status" +) +def dask_status(request): + dask_client = Client(settings.DASK_SCHEDULER_URL) + res = dask_client.run_on_scheduler( + lambda dask_scheduler: { + w: [(ts.key, ts.state) for ts in ws.processing] + for w, ws in dask_scheduler.workers.items() + } + ) + dask_client.close() + return sum(len(running_tasks) for running_tasks in res.values()) diff --git a/orochi/templates/base.html b/orochi/templates/base.html index 70c59b71..04c4c3d4 100644 --- a/orochi/templates/base.html +++ b/orochi/templates/base.html @@ -205,7 +205,7 @@ // TASK STATUS setInterval(function () { $.ajax({ - url: "{% url 'website:dask_status' %}", + url: "{% url 'api:dask_status' %}", success: function (data) { $("#tasks_running").html(data.running); }, diff --git a/orochi/templates/rest_framework/api.html b/orochi/templates/rest_framework/api.html deleted file mode 100644 index f0a6d3a5..00000000 --- a/orochi/templates/rest_framework/api.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "rest_framework/base.html" %} -{% load i18n %} - -{% block branding %} -Orochi -{% endblock %} \ No newline at end of file diff --git a/orochi/templates/users/user_plugins.html b/orochi/templates/users/user_plugins.html index bd9998b1..641b78e1 100644 --- a/orochi/templates/users/user_plugins.html +++ b/orochi/templates/users/user_plugins.html @@ -50,7 +50,7 @@
-
@@ -190,7 +190,8 @@ content: 'Plugin installed.', type: 'success', delay: 5000 - }); }, + }); + }, error: function () { $.toast({ title: 'Plugin status!', @@ -241,11 +242,14 @@ e.preventDefault(); var plg = this; var plg_name = $(plg).data('name'); - var up = $(plg).data('up'); + $.ajaxSetup({ + headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } + }); + + var url = "{% url 'api:enable' name=123 enable=456 %}".replace(/123/, plg_name).replace(/456/, plg.checked); $.ajax({ - url: "{% url 'website:enable_plugin' %}", - data: { 'plugin': up, 'enable': plg.checked, 'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken").val() }, + url: url, method: 'post', dataType: 'json', success: function (data) { diff --git a/orochi/templates/website/index.html b/orochi/templates/website/index.html index 1ea55af1..d9528441 100644 --- a/orochi/templates/website/index.html +++ b/orochi/templates/website/index.html @@ -523,9 +523,20 @@
History Log
$(document).on("submit", "#create-folder", function (e) { e.preventDefault(); var form = $(this); + let formData = form.serializeArray(); + let obj = {}; + formData.forEach(item=>{ + if(item.name != 'csrfmiddlewaretoken'){ + obj[item.name] = item.value; + } + }); + $.ajaxSetup({ + headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } + }); $.ajax({ url: form.attr("action"), - data: form.serialize(), + data: JSON.stringify(obj), + contentType: "application/json", type: form.attr("method"), dataType: 'json', success: function (data) { diff --git a/orochi/templates/website/partial_folder.html b/orochi/templates/website/partial_folder.html index 272ea6ec..4cfd1480 100644 --- a/orochi/templates/website/partial_folder.html +++ b/orochi/templates/website/partial_folder.html @@ -1,6 +1,6 @@ {% load widget_tweaks %} -
+ {{ form.media }} {% csrf_token %}