Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR Template #371

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Describe your changes

## Secure Software Development Lifecycle
- [ ] High Level Data Flow Diagrams Exist for Feature/Function?
- [ ] Initial Threat Modeling table has been completed against diagram?
- [ ] Have code changes been validated against [OWASP Top 10?](https://owasp.org/www-project-top-ten/)
- [A01:2021 - Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)
- [A02:2021 - Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/)
- [A03:2021 - Injection](https://owasp.org/Top10/A03_2021-Injection/)
- [A04:2021 - Insecure Design](https://owasp.org/Top10/A04_2021-Insecure_Design/)
- [A05:2021 - Security Misconfiguration](https://owasp.org/Top10/A05_2021-Security_Misconfiguration/)
- [A06:2021 - Vulnerable and Outdated Components](https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/)
- [A07:2021 - Identification and Authentication Failures](https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/)
- [A08:2021 - Software and Data Integrity Failures](https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/)
- [A09:2021 - Security Logging and Monitoring Failures](https://owasp.org/Top10/A09_2021-Security_Logging_and_Monitoring_Failures/)
- [A10:2021 - Server-Side Request Forgery](https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/)
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ DOCKER_TAG := ${VERSION}
DOCKER_IMAGE := ${DOCKER_OWNER}/${DOCKER_APP}:$(DOCKER_TAG)
SECRET_KEY := $(shell openssl rand -base64 12)
APP_LIST ?= api appstore core frontend middleware product
BRANDS := braini bdc heal restartr scidas eduhelx argus tracs eduhelx-sandbox eduhelx-dev
BRANDS := braini bdc heal restartr scidas eduhelx argus tracs eduhelx-sandbox eduhelx-dev eduhelx-dev-student eduhelx-dev-professor eduhelx-student eduhelx-professor
MANAGE := ${PYTHON} appstore/manage.py
SETTINGS_MODULE := ${DJANGO_SETTINGS_MODULE}

Expand Down Expand Up @@ -55,7 +55,7 @@ LOG_LEVEL := "info"
endif

ifeq "${DEBUG}" "true"
LOG_LEVEL := "debug"
LOG_LEVEL := DEBUG
endif

ifeq "${ENVS_FROM_FILE}" "true"
Expand Down
44 changes: 32 additions & 12 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 @@ -225,7 +225,8 @@ def to_bytes(memory):

# TODO fetch by user instead of iterating all?
# sanitize input to avoid injection.
def get_social_tokens(username):
def get_social_tokens(request):
username = request.user
social_token_model_objects = (
ContentType.objects.get(model="socialtoken").model_class().objects.all()
)
Expand All @@ -248,6 +249,10 @@ def get_social_tokens(username):
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 @@ -454,12 +459,12 @@ def get_serializer_class(self):
return InstanceSerializer

@functools.lru_cache(maxsize=16, typed=False)
def get_principal(self, user):
def get_principal(self, request):
"""
Retrieve principal information from Tycho based on the request
user.
"""
tokens = get_social_tokens(user)
tokens = get_tokens(request)
principal = Principal(*tokens)
return principal

Expand Down Expand Up @@ -498,7 +503,7 @@ def list(self, request):
"""

active = self.get_queryset()
principal = self.get_principal(request.user)
principal = self.get_principal(request)
username = principal.username
host = get_host(request)
instances = []
Expand Down Expand Up @@ -542,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 @@ -551,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.user)

#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 @@ -588,9 +597,15 @@ def create(self, request):
if validation_response is not None:
return validation_response

env = {}
if settings.GRADER_API_URL is not None:
env["GRADER_API_URL"] = settings.GRADER_API_URL

host = get_host(request)
system = tycho.start(principal, app_id, resource_request.resources, host)
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 @@ -620,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 All @@ -629,7 +645,7 @@ def retrieve(self, request, sid=None):
"""
Provide active instance details.
"""
principal = self.get_principal(request.user)
principal = self.get_principal(request)
username = principal.username
host = get_host(request)
instance = None
Expand All @@ -646,7 +662,7 @@ def retrieve(self, request, sid=None):

@action(detail=True, methods=['get'])
def is_ready(self, request, sid=None):
principal = self.get_principal(request.user)
principal = self.get_principal(request)
username = principal.username
host = get_host(request)
instance = None
Expand All @@ -673,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 All @@ -691,7 +711,7 @@ def partial_update(self, request, sid=None):
data = serializer.validated_data
data.update({"tycho-guid": sid})

principal = self.get_principal(request.user)
principal = self.get_principal(request)
username = principal.username
host = get_host(request)
instance = self.get_instance(sid,username,host)
Expand Down
2 changes: 2 additions & 0 deletions appstore/appstore/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@
"allauth.account.middleware.AccountMiddleware"
]

GRADER_API_URL = os.environ.get("GRADER_API_URL", None)

SESSION_IDLE_TIMEOUT = int(os.environ.get("DJANGO_SESSION_IDLE_TIMEOUT", 300))
EXPORTABLE_ENV = os.environ.get("EXPORTABLE_ENV",None)
if EXPORTABLE_ENV != None: EXPORTABLE_ENV = EXPORTABLE_ENV.split(':')
Expand Down
14 changes: 14 additions & 0 deletions appstore/appstore/settings/eduhelx-dev-professor_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .base import *
from product.configuration import ProductSettings, ProductColorScheme, ProductLink

# TODO remove Application brand once the new frontend is complete and
# the django templates in core are removed.
APPLICATION_BRAND = "eduhelx-dev-professor"

PRODUCT_SETTINGS = ProductSettings(
brand="eduhelx-dev-professor",
title="EduHeLx Dev Professor",
logo_url="/static/images/eduhelx-dev-professor/logo.png",
color_scheme=ProductColorScheme("#666666", "#e6e6e6"), #TBD
links=[],
)
14 changes: 14 additions & 0 deletions appstore/appstore/settings/eduhelx-dev-student_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .base import *
from product.configuration import ProductSettings, ProductColorScheme, ProductLink

# TODO remove Application brand once the new frontend is complete and
# the django templates in core are removed.
APPLICATION_BRAND = "eduhelx-dev-student"

PRODUCT_SETTINGS = ProductSettings(
brand="eduhelx-dev-student",
title="EduHeLx Dev Student",
logo_url="/static/images/eduhelx-dev-student/logo.png",
color_scheme=ProductColorScheme("#666666", "#e6e6e6"), #TBD
links=[],
)
14 changes: 14 additions & 0 deletions appstore/appstore/settings/eduhelx-professor_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .base import *
from product.configuration import ProductSettings, ProductColorScheme, ProductLink

# TODO remove Application brand once the new frontend is complete and
# the django templates in core are removed.
APPLICATION_BRAND = "eduhelx-professor"

PRODUCT_SETTINGS = ProductSettings(
brand="eduhelx-professor",
title="EduHeLx Professor",
logo_url="/static/images/eduhelx-professor/logo.png",
color_scheme=ProductColorScheme("#666666", "#e6e6e6"), #TBD
links=[],
)
14 changes: 14 additions & 0 deletions appstore/appstore/settings/eduhelx-student_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .base import *
from product.configuration import ProductSettings, ProductColorScheme, ProductLink

# TODO remove Application brand once the new frontend is complete and
# the django templates in core are removed.
APPLICATION_BRAND = "eduhelx-student"

PRODUCT_SETTINGS = ProductSettings(
brand="eduhelx-student",
title="EduHeLx",
logo_url="/static/images/eduhelx-student/logo.png",
color_scheme=ProductColorScheme("#666666", "#e6e6e6"), #TBD
links=[],
)
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.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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"),
]
28 changes: 28 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 @@ -57,6 +59,11 @@ def get_brand_details(brand):
"heal": {"name": "NIH Heal Initiative", "logo": "logo.png"},
"argus": {"name": "Argus Array", "logo": "logo.png"},
"eduhelx": {"name": "EduHelx", "logo": "logo.png"},
"eduhelx-dev": {"name": "EduHelx", "logo": "logo.png"},
"eduhelx-dev-student": {"name": "EduHelx", "logo": "logo.png"},
"eduhelx-dev-professor": {"name": "EduHelx", "logo": "logo.png"},
"eduhelx-student": {"name": "EduHelx", "logo": "logo.png"},
"eduhelx-professor": {"name": "EduHelx", "logo": "logo.png"},
"testing": {"name": "Testing", "logo": "logo.png"},
"ordrd": {"name": "Ordr D", "logo": "logo.png"},
}[brand]
Expand All @@ -74,6 +81,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("The token is expired. Try restarting the app.", status=401)
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. Try restarting the app.", status=401)


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