Skip to content

Commit

Permalink
Add identity access token system
Browse files Browse the repository at this point in the history
  • Loading branch information
frostyfan109 committed May 25, 2024
1 parent d55ca72 commit e85828f
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 15 deletions.
41 changes: 28 additions & 13 deletions appstore/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from allauth import socialaccount

from tycho.context import ContextFactory, Principal
from core.models import IrodAuthorizedUser
from core.models import IrodAuthorizedUser, UserIdentityToken

from .models import Instance, InstanceSpec, App, LoginProvider, Resources, User
from .serializers import (
Expand Down Expand Up @@ -230,15 +230,15 @@ def get_social_tokens(request):
social_token_model_objects = (
ContentType.objects.get(model="socialtoken").model_class().objects.all()
)
access_token = request.COOKIES.get("sessionid")
access_token = None
refresh_token = None
# for obj in social_token_model_objects:
# if obj.account.user.username == username:
# access_token = obj.token
# refresh_token = obj.token_secret if obj.token_secret else None
# break
# else:
# continue
for obj in social_token_model_objects:
if obj.account.user.username == username:
access_token = obj.token
refresh_token = obj.token_secret if obj.token_secret else None
break
else:
continue
# with DRF and the user interaction in social auth we need username to be a string
# when it is passed to `tycho.start` otherwise it will be a `User` object and there
# will be a serialization failure from this line of code:
Expand All @@ -249,6 +249,10 @@ def get_social_tokens(request):
return str(username), access_token, refresh_token


def get_tokens(request):
username = request.user.get_username()
return username, None, None

class AppViewSet(viewsets.GenericViewSet):
"""
AppViewSet - ViewSet for managing Tycho apps.
Expand Down Expand Up @@ -460,7 +464,7 @@ def get_principal(self, request):
Retrieve principal information from Tycho based on the request
user.
"""
tokens = get_social_tokens(request)
tokens = get_tokens(request)
principal = Principal(*tokens)
return principal

Expand Down Expand Up @@ -543,6 +547,8 @@ def create(self, request):
Given an app id and resources pass the information to Tycho to start
a instance of an app.
"""

username = request.user.get_username()

serializer = self.get_serializer(data=request.data)
logging.debug("checking if request is valid")
Expand All @@ -552,13 +558,15 @@ def create(self, request):
logging.debug(f"resource_request: {resource_request}")
irods_enabled = os.environ.get("IROD_HOST",'').strip()
# TODO update social query to fetch user.
tokens = get_social_tokens(request)

#Need to set an environment variable for the IRODS UID
if irods_enabled != '':
nfs_id = get_nfs_uid(request.user)
nfs_id = get_nfs_uid(username)
os.environ["NFSRODS_UID"] = str(nfs_id)

principal = Principal(*tokens)
# We will update this later once a system id for the app exists
identity_token = UserIdentityToken.objects.create(user=request.user)
principal = Principal(username, identity_token.token, None)

app_id = serializer.data["app_id"]
app_data = tycho.apps.get(app_id)
Expand Down Expand Up @@ -596,6 +604,8 @@ def create(self, request):
host = get_host(request)
system = tycho.start(principal, app_id, resource_request.resources, host, env)

identity_token.consumer_id = identity_token.compute_app_consumer_id(app_id, system.identifier)
identity_token.save()

s = InstanceSpec(
principal.username,
Expand Down Expand Up @@ -625,6 +635,7 @@ def create(self, request):
# Failed to construct a tracked instance, attempt to remove
# potentially created instance rather than leaving it hanging.
tycho.delete({"name": system.services[0].identifier})
identity_token.delete()
return Response(
{"message": "failed to submit app start."},
status=drf_status.HTTP_500_INTERNAL_SERVER_ERROR,
Expand Down Expand Up @@ -678,6 +689,10 @@ def destroy(self, request, sid=None):
logger.info("request username: " + str(request.user.username))
if status.services[0].username == request.user.username:
response = tycho.delete({"name": serializer.validated_data["sid"]})
# Delete all the tokens the user had associated with that app
consumer_id = UserIdentityToken.compute_app_consumer_id(serializer.validated_data["aid"], serializer.validated_data["sid"])
tokens = UserIdentityToken.objects.filter(user=request.user, consumer_id=consumer_id)
tokens.delete()
# TODO How can we avoid this sleep? Do we need an immediate response beyond
# a successful submission? Can we do a follow up with Web Sockets or SSE
# to the front end?
Expand Down
3 changes: 3 additions & 0 deletions appstore/core/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

class AppsCoreServicesConfig(AppConfig):
name = 'core'

def ready(self):
import core.signals
36 changes: 35 additions & 1 deletion appstore/core/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import secrets
from django.db import models
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.contrib.sessions.models import Session as SessionModel
from django.core.exceptions import ValidationError
from django_saml2_auth.user import get_user
from datetime import timedelta
from string import ascii_letters, digits, punctuation

UserModel = get_user_model()

def generate_token():
token = "".join(secrets.choice(ascii_letters + digits) for i in range(256))
# Should realistically never occur, but it's possible.
while UserIdentityToken.objects.filter(token=token).exists():
token = "".join(secrets.choice(ascii_letters + digits) for i in range(256))
return token

def update_user(user):
# as of Django_saml2_auth v3.12.0 does not add email address by default
Expand Down Expand Up @@ -30,4 +44,24 @@ class IrodAuthorizedUser(models.Model):
uid = models.IntegerField()

def __str__(self):
return f"{self.user}, {self.uid}"
return f"{self.user}, {self.uid}"

class UserIdentityToken(models.Model):
user = models.ForeignKey(UserModel, on_delete=models.CASCADE)

token = models.CharField(max_length=256, unique=True, default=generate_token)
# Optionally, identify the consumer (probably an app) whom the token was generated for.
consumer_id = models.CharField(max_length=256, default=None, null=True)
expires = models.DateTimeField(default=timezone.now() + timedelta(days=31))

@property
def valid(self):
return timezone.now() <= self.expires

@staticmethod
def compute_app_consumer_id(app_id, system_id):
return f"{ app_id }-{ system_id }"

def __str__(self):
return f"{ self.user.get_username() }-token-{ self.pk }"

Empty file added appstore/core/signals.py
Empty file.
5 changes: 4 additions & 1 deletion appstore/core/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.urls import path, re_path
from .views import auth, index, HandlePrivateURL404s
from .views import auth, auth_identity, index, HandlePrivateURL404s

urlpatterns = [
path('default/', index, name='index'),
# Auth based on sessionid cookie
path("auth/", auth, name="auth"),
# Auth based on identity access token
path("auth/identity/", auth_identity, name="auth-identity"),
re_path(r"^private/*", HandlePrivateURL404s, name="private"),
]
23 changes: 23 additions & 0 deletions appstore/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect

from core.models import UserIdentityToken

from tycho.context import ContextFactory

from urllib.parse import urljoin
Expand Down Expand Up @@ -74,6 +76,27 @@ def get_access_token(request):
return access_token


def auth_identity(request):
auth_header = request.headers.get("Authorization")
try:
bearer, raw_token = auth_header.split(" ")
if bearer != "Bearer": raise ValueError()
except:
return HttpResponse("Authorization header must be structured as 'Bearer {token}'", status=400)

try:
token = UserIdentityToken.objects.get(token=raw_token)
if not token.valid:
return HttpResponse()
remote_user = token.user.get_username()
response = HttpResponse(remote_user, status=200)
response["REMOTE_USER"] = remote_user
response["ACCESS_TOKEN"] = token.token
return response
except UserIdentityToken.DoesNotExist:
return HttpResponse("The token does not exist", status=401)


@login_required
def auth(request):
"""Provide an endpoint for getting the user identity.
Expand Down

0 comments on commit e85828f

Please sign in to comment.