Skip to content

Commit

Permalink
added redeem logic
Browse files Browse the repository at this point in the history
  • Loading branch information
jhnnsrs committed Apr 2, 2024
1 parent fd11b04 commit ebb4667
Show file tree
Hide file tree
Showing 15 changed files with 284 additions and 11 deletions.
2 changes: 1 addition & 1 deletion contrib/builders/arkitekt.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def lok(self: "SelfServiceDescriptor", context: "LinkingContext", descriptor: "D



return { "base_url": base_url + "/o", "userinfo_url ": f"{base_url}/o/userinfo", "token_url": f"{base_url}/o/token", "authorization_url": f"{base_url}/o/authorize", "client_id": context.client.client_id, "client_secret": context.client.client_secret, "client_type": context.client.client_type, "authorization_grant_type": context.client.authorization_grant_type, "name": context.client.name, "scopes": context.manifest.scopes, "__service": "live.arkitekt.lok"} | generic(self, context, descriptor)
return { "base_url": base_url + "/o", "userinfo_url ": f"{base_url}/o/userinfo", "token_url": f"{base_url}/o/token", "authorization_url": f"{base_url}/o/authorize", "client_id": context.client.client_id, "client_secret": context.client.client_secret, "client_type": context.client.client_type, "grant_type": context.client.authorization_grant_type, "name": context.client.name, "scopes": context.manifest.scopes, "__service": "live.arkitekt.lok"} | generic(self, context, descriptor)


def generic(self: "SelfServiceDescriptor", context: "LinkingContext", descriptor: "DockerServiceDescriptor"):
Expand Down
1 change: 1 addition & 0 deletions fakts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
admin.site.register(Client)
admin.site.register(DeviceCode)
admin.site.register(ServiceInstanceMapping)
admin.site.register(RedeemToken)
7 changes: 7 additions & 0 deletions fakts/base_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ class DeviceCodeStartRequest(BaseModel):
request_public: bool = False



class ReedeemTokenRequest(BaseModel):
""" A RedeemTokenRequest is used to redeem a token for a development client. It only contains the token."""
token: str
manifest: Manifest


class DeviceCodeChallengeRequest(BaseModel):
""" A DeviceCodeChallengeRequest is used to start the device code flow. It only
contains the device code."""
Expand Down
8 changes: 6 additions & 2 deletions fakts/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,12 @@ def create_client(
):
from .utils import download_logo


logo = download_logo(manifest.logo) if manifest.logo else None
try:
logo = download_logo(manifest.logo) if manifest.logo else None
except Exception as e:
raise ValueError(f"Could not download logo {e}")


app, _ = models.App.objects.get_or_create(identifier=manifest.identifier)
release, _ = models.Release.objects.update_or_create(app=app, version=manifest.version, defaults={
"logo": logo,
Expand Down
4 changes: 2 additions & 2 deletions fakts/graphql/queries/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from fakts import enums, inputs, models, scalars, types


def client(info: Info, id: strawberry.ID | None, client_id: strawberry.ID | None) -> types.Client:
return models.Client.get(id=id)
def client(info: Info, id: strawberry.ID | None, client_id: strawberry.ID | None = None) -> types.Client:
return models.Client.objects.get(id=id)



Expand Down
7 changes: 3 additions & 4 deletions fakts/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def auto_create_composition(manifest: base_models.Manifest) -> models.Compositio

except Exception as e:
if req.optional:
continue
warnings.append(str(e))
else:
errors.append(str(e))

Expand Down Expand Up @@ -122,14 +122,13 @@ def check_compability(manifest: base_models.Manifest) -> list[str] | list[str]:
try:
service = models.Service.objects.get(identifier=req.service)
except models.Service.DoesNotExist:
errors.append(f"Service {req.service} not found on this server. Please contact the administrator.")
continue
raise Exception(f"Service {req.service} not found on this server. Please contact the administrator.")

instance = find_instance_for_requirement(service, req)

except Exception as e:
if req.optional:
continue
warnings.append(str(e))
else:
errors.append(str(e))

Expand Down
53 changes: 53 additions & 0 deletions fakts/management/commands/ensuretokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.conf import settings
import os
from fakts import models, base_models
import yaml
from fakts.scan import scan
from fakts.backends.instances import registry


# import required module
from pathlib import Path


# assign directory
directory = "files"

# iterate over files in
# that directory


class Command(BaseCommand):
help = "Creates an admin user non-interactively if it doesn't exist"

def handle(self, *args, **kwargs):

TOKENS = settings.REDEEM_TOKENS

for token in TOKENS:

user = get_user_model().objects.get(username=token["user"])

token, _ = models.RedeemToken.objects.update_or_create(
token=token["token"],
defaults={
"user": user,
}
)

print(f"Token {token} created for user {user}")













69 changes: 69 additions & 0 deletions fakts/migrations/0007_alter_devicecode_staging_kind_redeemtoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Generated by Django 4.2.5 on 2024-04-02 10:29

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_choices_field.fields
import fakts.enums
import uuid


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("fakts", "0006_alter_release_requirements_instanceconfig_and_more"),
]

operations = [
migrations.AlterField(
model_name="devicecode",
name="staging_kind",
field=django_choices_field.fields.TextChoicesField(
choices=[
("website", "WEBSITE (Value represent WEBSITE)"),
("development", "DEVELOPMENT (Value represent DEVELOPMENT)"),
("desktop", "DESKTOP (Value represent DESKTOP Aüü)"),
],
choices_enum=fakts.enums.ClientKindChoices,
default="development",
help_text="The kind of staging client",
max_length=11,
),
),
migrations.CreateModel(
name="RedeemToken",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"token",
models.CharField(default=uuid.uuid4, max_length=1000, unique=True),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("expires_at", models.DateTimeField(null=True)),
(
"client",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="redeemed_client",
to="fakts.client",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issued_tokens",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
23 changes: 23 additions & 0 deletions fakts/migrations/0008_alter_redeemtoken_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.5 on 2024-04-02 10:30

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


class Migration(migrations.Migration):
dependencies = [
("fakts", "0007_alter_devicecode_staging_kind_redeemtoken"),
]

operations = [
migrations.AlterField(
model_name="redeemtoken",
name="client",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="redeemed_client",
to="fakts.client",
),
),
]
19 changes: 18 additions & 1 deletion fakts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,27 @@ class Meta:
]

def __str__(self):
return f"{self.key}:{self.instance}@{self.composition}"
return f"{self.key}:{self.instance}@{self.composition}"




class RedeemToken(models.Model):
""" A redeem token is a token that can be used to redeed the rights to create
a client. It is used to give the recipient the right to create a client.
If the token is not redeemed within the expires_at time, it will be invalid.
If the token has been redeemed, but the manifest has changed, the token will be invalid.
"""
client = models.OneToOneField("Client", on_delete=models.CASCADE, related_name="redeemed_client", null=True)
token = models.CharField(max_length=1000, unique=True, default=uuid.uuid4)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField(null=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name="issued_tokens")


class DeviceCode(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
code = models.CharField(max_length=100, unique=True)
Expand Down
1 change: 1 addition & 0 deletions fakts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
base_urlpatterns = [
re_path(r"^configure/$", views.ConfigureView.as_view(), name="configure"),
re_path(r"^retrieve/$", views.RetrieveView.as_view(), name="retrieve"),
re_path(r"^redeem/$", views.RedeemView.as_view(), name="redeem"),
re_path(r"^challenge/$", views.ChallengeView.as_view(), name="challenge"),
re_path(r"^start/$", views.StartChallengeView.as_view(), name="start"),
re_path(r"^device/$", views.DeviceView.as_view(), name="device"),
Expand Down
93 changes: 93 additions & 0 deletions fakts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,10 @@ def post(self, request, *args, **kwargs):
"message": "User has not verfied the challenge",
}
)






@method_decorator(csrf_exempt, name="dispatch")
Expand Down Expand Up @@ -479,6 +483,95 @@ def post(self, request, *args, **kwargs):
)


@method_decorator(csrf_exempt, name="dispatch")
class RedeemView(View):
"""
Implements an endpoint that returns the faktsclaim for a given identifier and version
if the app was already configured and the app is marked as PUBLIC. While any app can
request a faktsclaim for any other app, redirect uris are set to predifined values
and the app will not be able to use the faktsclaim to get a configuration.
"""

def post(self, request, *args, **kwargs):
json_data = json.loads(request.body)
try:
redeem_request = base_models.ReedeemTokenRequest(**json_data)
except Exception as e:
logger.error(e, exc_info=True)
return JsonResponse(
data={
"status": "error",
"error": f"Malformed request: {str(e)}",
}
)


manifest = redeem_request.manifest
token = redeem_request.token

try:
valid_token = models.RedeemToken.objects.get(token=token)
except models.RedeemToken.DoesNotExist:
return JsonResponse(
data={
"status": "error",
"message": "Invalid redeem token",
}
)
if valid_token.expires_at:
if valid_token.expires_at < datetime.datetime.now(timezone.utc):
return JsonResponse(
data={
"status": "error",
"message": "Redeem token expired",
}
)

if valid_token.client:
return JsonResponse(
data={
"status": "granted",
"token": valid_token.client.token,
}
)

else:
try:
token = logic.create_api_token()

config = None

config = base_models.DevelopmentClientConfig(
kind=enums.ClientKindVanilla.DEVELOPMENT.value,
token=token,
user=valid_token.user.username,
tenant=valid_token.user.username,
)


client = builders.create_client(
manifest=manifest,
config=config,
)

valid_token.client = client
valid_token.save()

return JsonResponse(
data={
"status": "granted",
"token": client.token,
}
)
except Exception as e:
logger.error(e, exc_info=True)
return JsonResponse(
data={
"status": "error",
"message": str(e),
}
)


@method_decorator(csrf_exempt, name="dispatch")
class ClaimView(View):
Expand Down
2 changes: 1 addition & 1 deletion komment/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ class DescendantKind(str, Enum):

LEAF = "LEAF"
MENTION = "MENTION"
PARAGRAPH = "PARGRAPTH"
PARAGRAPH = "PARAGRAPH"

3 changes: 3 additions & 0 deletions lok/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
ASGI_APPLICATION = "lok.asgi.application"


REDEEM_TOKENS = conf.get("redeem_tokens", [])


EKKE = {
"PUBLIC_KEY": conf.lok.get("public_key", None),
"PUBLIC_KEY_PEM_FILE": conf.lok.get("public_key_pem_file", None),
Expand Down
3 changes: 3 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ python manage.py ensureusers
echo "=> Ensuring Compositions..."
python manage.py ensurecompositions

echo "=> Ensuring Redeem Tokens..."
python manage.py ensuretokens

echo "=> Ensuring Apps..."
python manage.py ensureapps

Expand Down

0 comments on commit ebb4667

Please sign in to comment.