Skip to content

Commit

Permalink
Merge pull request #77 from CentreForDigitalHumanities/develop
Browse files Browse the repository at this point in the history
3.2
  • Loading branch information
tymees authored Nov 12, 2024
2 parents ee9b4f1 + 6ab788e commit 1e35db2
Show file tree
Hide file tree
Showing 82 changed files with 3,784 additions and 261 deletions.
3 changes: 3 additions & 0 deletions agent/humitifier/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ class Config:
inventory: list[str]
pssh: dict
tasks: dict[str, str]
token_endpoint: str
upload_endpoint: str
client_id: str
client_secret: str

@classmethod
def load(cls) -> "Config":
Expand Down
49 changes: 43 additions & 6 deletions agent/humitifier/tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import base64

import asyncio
from sys import stdout

Expand Down Expand Up @@ -106,6 +108,33 @@ async def scan_hosts():
)
await conn.close()

async def request_oauth_token():
credential = "{0}:{1}".format(CONFIG.client_id, CONFIG.client_secret)
encoded_credentials = base64.b64encode(credential.encode("utf-8")).decode("utf-8")

headers = {
"Authorization": f"Basic {encoded_credentials}",
"Cache-Control": "no-cache",
"Content-Type": "application/x-www-form-urlencoded"
}

data= {
"grant_type": "client_credentials",
"scope": "system"
}

response = await asyncio.to_thread(
requests.post,
CONFIG.token_endpoint,
headers=headers,
data=data,
)
print(response.json())
json_data = response.json()
if "error" in json_data:
return None
return json_data["access_token"]


@app.task(after_success(scan_hosts))
async def parse_facts():
Expand All @@ -129,12 +158,20 @@ async def parse_facts():
}
)

await asyncio.to_thread(
requests.post,
CONFIG.upload_endpoint,
data=json.dumps(host_data),
headers={'Content-Type': 'application/json'}
)
token = await request_oauth_token()

if token:
await asyncio.to_thread(
requests.post,
CONFIG.upload_endpoint,
data=json.dumps(host_data),
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
)
else:
logger.error("Failed to get OAuth token")

await conn.executemany(
"""INSERT INTO facts(name, host, scan, data) VALUES($1, $2, $3, $4)""",
Expand Down
2 changes: 1 addition & 1 deletion agent/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "humitifier"
version = "3.0.0"
version = "3.2.0"
description = "Tools and interfaces for displaying server resources"
authors = ["Donatas Rasiukevicius <[email protected]>"]

Expand Down
2 changes: 1 addition & 1 deletion humitifier-server/docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ source /app/.venv/bin/activate
python src/manage.py migrate

# Run da server
gunicorn humitifier_server.wsgi:application -c gunicorn.conf.py "$@"
exec gunicorn humitifier_server.wsgi:application -c gunicorn.conf.py "$@"
517 changes: 516 additions & 1 deletion humitifier-server/poetry.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion humitifier-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "humitifier-server"
version = "0.1.0"
version = "3.2.0"
description = ""
authors = ["Mees, T.D. (Ty) <[email protected]>"]

Expand All @@ -16,6 +16,9 @@ django-debug-toolbar = "^4.4.6"
django-simple-menu = "^2.1.3"
whitenoise = "^6.8.2"
sentry-sdk = {extras = ["django"], version = "^2.17.0"}
mozilla-django-oidc = "^4.0.1"
drf-spectacular = "^0.27.2"
django-oauth-toolkit = "^3.0.1"


[tool.poetry.group.dev.dependencies]
Expand Down
11 changes: 11 additions & 0 deletions humitifier-server/src/api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import django_filters

from api.models import OAuth2Application
from main.filters import FiltersForm


class OAuth2ApplicationFilters(django_filters.FilterSet):
class Meta:
model = OAuth2Application
fields = ['access_profile']
form = FiltersForm
33 changes: 33 additions & 0 deletions humitifier-server/src/api/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django import forms
from django.conf import settings

from api.models import OAuth2Application

class ScopeWidget(forms.CheckboxSelectMultiple):

def format_value(self, value):
if value is None:
return []
return value.split(',')

def optgroups(self, name, value, attrs=None):
self.choices = [
(scope, scope)
for scope in set(settings.OAUTH2_PROVIDER['SCOPES'].keys())
]

return super().optgroups(name, value, attrs)

class OAuth2ApplicationForm(forms.ModelForm):
class Meta:
model = OAuth2Application
fields = [
'name',
'client_id',
'client_secret',
'access_profile',
'allowed_scopes',
]
widgets = {
'allowed_scopes': ScopeWidget,
}
25 changes: 25 additions & 0 deletions humitifier-server/src/api/menus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.urls import reverse
from simple_menu import Menu, MenuItem


Menu.add_item(
"main",
MenuItem(
"API documentation",
reverse("api:redoc"),
weight=22,
icon="icons/document.html",
separator=True,
)
)

Menu.add_item(
"main",
MenuItem(
"OAuth2 Applications",
reverse("api:oauth_applications"),
weight=23,
check=lambda request: request.user.is_superuser,
icon="icons/api.html",
)
)
137 changes: 137 additions & 0 deletions humitifier-server/src/api/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Generated by Django 5.1.2 on 2024-11-11 19:54

import django.contrib.postgres.fields
import django.db.models.deletion
import oauth2_provider.generators
import oauth2_provider.models
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("main", "0004_alter_user_default_home_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="OAuth2Application",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"client_id",
models.CharField(
db_index=True,
default=oauth2_provider.generators.generate_client_id,
max_length=100,
unique=True,
),
),
(
"redirect_uris",
models.TextField(
blank=True, help_text="Allowed URIs list, space separated"
),
),
(
"post_logout_redirect_uris",
models.TextField(
blank=True,
default="",
help_text="Allowed Post Logout URIs list, space separated",
),
),
(
"client_type",
models.CharField(
choices=[
("confidential", "Confidential"),
("public", "Public"),
],
max_length=32,
),
),
(
"authorization_grant_type",
models.CharField(
choices=[
("authorization-code", "Authorization code"),
("implicit", "Implicit"),
("password", "Resource owner password-based"),
("client-credentials", "Client credentials"),
("openid-hybrid", "OpenID connect hybrid"),
],
max_length=32,
),
),
(
"client_secret",
oauth2_provider.models.ClientSecretField(
blank=True,
db_index=True,
default=oauth2_provider.generators.generate_client_secret,
help_text="Hashed on Save. Copy it now if this is a new secret.",
max_length=255,
),
),
("hash_client_secret", models.BooleanField(default=True)),
("name", models.CharField(blank=True, max_length=255)),
("skip_authorization", models.BooleanField(default=False)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"algorithm",
models.CharField(
blank=True,
choices=[
("", "No OIDC support"),
("RS256", "RSA with SHA-2 256"),
("HS256", "HMAC with SHA-2 256"),
],
default="",
max_length=5,
),
),
(
"allowed_origins",
models.TextField(
blank=True,
default="",
help_text="Allowed origins list to enable CORS, space separated",
),
),
(
"allowed_scopes",
django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=20), size=None
),
),
(
"access_profile",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="oauth_applications",
to="main.accessprofile",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.1.2 on 2024-11-12 12:57

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0001_initial"),
("main", "0004_alter_user_default_home_and_more"),
]

operations = [
migrations.AlterField(
model_name="oauth2application",
name="access_profile",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="oauth_applications",
to="main.accessprofile",
),
),
]
25 changes: 24 additions & 1 deletion humitifier-server/src/api/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from jsonschema.exceptions import ValidationError
from oauth2_provider.models import AbstractApplication

# Create your models here.

class OAuth2Application(AbstractApplication):
access_profile = models.ForeignKey(
"main.AccessProfile",
on_delete=models.SET_NULL,
related_name="oauth_applications",
blank=True,
null=True,
)
allowed_scopes = ArrayField(
models.CharField(max_length=20),
)

def clean(self):
super().clean()
if self.allowed_scopes:
allowed_scopes_set = set(self.allowed_scopes)
available_scopes_set = set(settings.OAUTH2_PROVIDER['SCOPES'].keys())
if not allowed_scopes_set.issubset(available_scopes_set):
raise ValidationError("Invalid scopes in allowed_scopes")
16 changes: 16 additions & 0 deletions humitifier-server/src/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from oauth2_provider.models import AccessToken
from rest_framework import permissions


class TokenHasApplication(permissions.BasePermission):
def has_permission(self, request, view):
token = request.auth
if not token:
return False

try:
access_token = AccessToken.objects.get(token=token)
request.application = access_token.application
return True
except AccessToken.DoesNotExist:
return False
Loading

0 comments on commit 1e35db2

Please sign in to comment.