Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
a25cb55
remove aid info from app identity token
frostyfan109 Oct 28, 2024
f4e9211
Fix log level bug in makefile
frostyfan109 Mar 19, 2025
3e39569
change tycho info logging to debug level
frostyfan109 Mar 19, 2025
f94156b
add select info logs back to tycho
frostyfan109 Mar 19, 2025
aca28b9
add info log for app termination
frostyfan109 Mar 19, 2025
06529c8
add further logging changes and authentication signal logging
frostyfan109 Apr 3, 2025
2197af1
add default helx favicon for api logging purposes
frostyfan109 Apr 3, 2025
885bb27
downgrade 403 list users endpoint to debug log, fix signal kwarg bug,…
frostyfan109 Apr 3, 2025
04f3299
downgrade debug-toolbar back to original version due to version confl…
frostyfan109 Apr 3, 2025
2619a0b
Merge branch 'remove-aid-apptoken' of https://github.com/helxplatform…
frostyfan109 Apr 3, 2025
3d1cf61
Restore tycho logging. Set tycho log level to warning+. Move app star…
frostyfan109 Apr 3, 2025
0af823a
move log order in app launch order, hide delete prints in tycho
frostyfan109 Apr 3, 2025
3295ab4
update dockerfile
frostyfan109 Apr 4, 2025
4dcd98b
revert to python3.9 to avoid pyyaml/cython conflict
frostyfan109 Apr 4, 2025
6f61803
upgrade from python 3.9 patch 18 to 21
frostyfan109 Apr 4, 2025
59209ff
remove pvc collectiondelete that lacked permission
frostyfan109 Apr 4, 2025
13ba2ed
Merge pull request #377 from helxplatform/audit-logging
frostyfan109 Apr 9, 2025
3e9aec3
Set the enableServiceLins to false without exception
waTeim May 1, 2025
13fdf5c
Upgrade Django to latest 4.2 patch, remove superfluous flask requirem…
frostyfan109 Jun 11, 2025
5db139b
update gitpython patch
frostyfan109 Jun 11, 2025
ef725bf
delete superfluous sqlparse req
frostyfan109 Jun 11, 2025
6cf27b7
Upgrade to python 3.9.23
frostyfan109 Jun 11, 2025
bed836a
upgrade gunicorn to 23.0.0
frostyfan109 Jun 11, 2025
8e3f274
upgrade requests, jinja
frostyfan109 Jun 11, 2025
68247f8
upgrade drf
frostyfan109 Jun 11, 2025
0cf60f0
test bookworm image
frostyfan109 Jun 11, 2025
5f3ffd2
test remove build
frostyfan109 Jun 11, 2025
592b83d
also remove installs
frostyfan109 Jun 11, 2025
61df939
test removing libpq5 and gcc
frostyfan109 Jun 11, 2025
b379d34
test removing build-essential
frostyfan109 Jun 11, 2025
7df5cda
also remove build so scan runs
frostyfan109 Jun 11, 2025
00f64d6
remove build again
frostyfan109 Jun 11, 2025
fc99160
remove git
frostyfan109 Jun 11, 2025
2175b33
remove xmlsec
frostyfan109 Jun 11, 2025
c658fde
remove libpq5
frostyfan109 Jun 11, 2025
57cb0a6
remove gcc
frostyfan109 Jun 11, 2025
016aaf1
trying apt-get install make
joshua-seals Jun 12, 2025
64a5a4c
tweak syntax
joshua-seals Jun 12, 2025
5bb8e8b
Adding git back
joshua-seals Jun 12, 2025
f5433df
add xmlsec back
frostyfan109 Jun 12, 2025
543ab5a
test appstore on alpine
frostyfan109 Jun 18, 2025
aa2dcd0
Change dockerfile to hopefully work on alpine
frostyfan109 Jun 18, 2025
9f02cee
fix pyyaml wheel build?
frostyfan109 Jun 18, 2025
e224c15
Add linux-headers
frostyfan109 Jun 18, 2025
c67c1f9
remove netifaces
frostyfan109 Jun 18, 2025
ef404ff
remove netifaces import
frostyfan109 Jun 18, 2025
c732082
add debug
frostyfan109 Jun 18, 2025
071cf34
add openssl
frostyfan109 Jun 19, 2025
a6f48fd
upgrade urllib3
frostyfan109 Jun 19, 2025
e99170c
dockerfile alpine permission fixes
frostyfan109 Jun 19, 2025
ef1f992
upgrade requests-cache to resolve urllib3 conflict
frostyfan109 Jun 19, 2025
462f107
try to force cache invalidate
frostyfan109 Jun 19, 2025
7a73a63
remove permission change
frostyfan109 Jun 19, 2025
8f106a8
fix chown and remove debian install step
frostyfan109 Jun 19, 2025
7b12f49
run on non-root user
frostyfan109 Jun 19, 2025
8d4939c
fix typo
frostyfan109 Jun 19, 2025
c4e60af
more user perm tweaking.
frostyfan109 Jun 19, 2025
feee5af
install as root
frostyfan109 Jun 19, 2025
a3835ff
create symlink to /shared under /home/user/shared for ordrd deployments
frostyfan109 Jul 8, 2025
07016d1
Merge pull request #384 from helxplatform/shared-symlink
frostyfan109 Jul 9, 2025
0c62425
Feature; add ldap check for whitelist inclusion
waTeim Jul 24, 2025
3f216e8
Merge pull request #378 from helxplatform/security-upgrades
waTeim Aug 7, 2025
bb00354
resolve requests_cache conflict
waTeim Aug 7, 2025
e12f41d
Merge branch 'develop' into hlxk_396
waTeim Aug 7, 2025
baaf36c
Merge pull request #385 from helxplatform/hlxk_396
waTeim Aug 7, 2025
d8b83e5
README++
waTeim Aug 13, 2025
c3a85d1
Merge pull request #389 from helxplatform/hlxk_386
waTeim Aug 13, 2025
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
34 changes: 18 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
FROM python:3.9.18-slim-bullseye
FROM python:3.9.23-alpine

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Least privilege: Run as a non-root user.
ENV USER appstore
ENV APP_HOME /usr/src/inst-mgmt
ENV HOME /home/$USER
ENV UID 1000
ENV USER=appstore
ENV APP_HOME=/usr/src/inst-mgmt
ENV HOME=/home/$USER
ENV UID=1000

RUN mkdir $APP_HOME

RUN adduser --disabled-login --home $HOME --shell /bin/bash --uid $UID $USER && \
chown -R $UID:$UID $HOME

RUN set -x && apt-get update && \
chown -R $UID:$UID $APP_HOME && \
apt-get install -y build-essential git xmlsec1 libpq5 gcc
RUN set -x && \
apk add --no-cache make git bash build-base xmlsec libxml2-dev linux-headers openssl && \
adduser -D -s /bin/bash -h $HOME -u $UID $USER && \
chown -R $UID:$UID $APP_HOME

# Removing but leaving commented in case Tycho needs this for swagger.
# Version 3.3.1 currently, if not complaints v3.3.3 this can be
Expand All @@ -25,15 +23,19 @@ RUN set -x && apt-get update && \
# RUN apt-get install -y nodejs

WORKDIR $APP_HOME
COPY . .
COPY --chown=$UID:$UID . .

RUN chown -R $USER:0 $APP_HOME && \
chmod -R g+w $APP_HOME

RUN if [ -d whl -a "$(ls -A whl/*.whl)" ]; then pip install whl/*.whl; fi
RUN export SET_BUILD_ENV_FROM_FILE=false \
&& pip install "cython<3.0.0" wheel \
&& pip install "pyyaml==5.4.1" --no-build-isolation \
&& make install \
&& unset SET_BUILD_ENV_FROM_FILE

RUN chown -R 1000:0 /usr/src/inst-mgmt
RUN chmod -R g+w /usr/src/inst-mgmt
USER $USER

EXPOSE 8000
CMD ["make","start"]
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ DEBUG := false
endif

ifndef LOG_LEVEL
LOG_LEVEL := "info"
LOG_LEVEL := "INFO"
endif

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

ifeq "${ENVS_FROM_FILE}" "true"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ each in an application specific subfolder. Along with the docker compose, a `.en
file specifies environment variables for the application. If a file called icon.png
is provided, that is used as the application's icon.

### App Deployment features

1. Follows docker-compose as a baseline description
2. Best practices in constructing deployment definitions
a. disables service env file generation to avoid information leak


## Development Environment

### Prerequisites
Expand Down
28 changes: 18 additions & 10 deletions appstore/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import functools
import logging
from dataclasses import asdict
import time
import os
import re
from typing import Optional
from dataclasses import asdict

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
Expand Down Expand Up @@ -358,7 +359,7 @@ def list(self, request):
# TODO change this to serializer.data after discovery on nested object data
return Response(apps)

def retrieve(self, request, app_id=None):
def retrieve(self, request, app_id: Optional[str]=None):
"""
Provide app details.
"""
Expand Down Expand Up @@ -604,7 +605,7 @@ 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.consumer_id = identity_token.compute_app_consumer_id(system.identifier)
identity_token.save()

s = InstanceSpec(
Expand All @@ -623,17 +624,20 @@ def create(self, request):
serializer = InstanceSpecSerializer(data=asdict(s))
try:
serializer.is_valid(raise_exception=True)
logger.info(f"Launched app { app_id }-{ system.identifier } for user { username }")
return Response(serializer.validated_data)
except serializers.ValidationError:
except serializers.ValidationError as e:
# Delete invalid instance configuration that we won't be tracking
# for the user.
logger.error(f"Failed to launch app { app_id } for user { username }; exception: { str(e) }")
tycho.delete({"name": system.services[0].identifier})
return Response(
serializer.errors, status=drf_status.HTTP_400_BAD_REQUEST
)
else:
# Failed to construct a tracked instance, attempt to remove
# potentially created instance rather than leaving it hanging.
logger.error(f"Failed to launch app { app_id } for user { username }; null instance spec")
tycho.delete({"name": system.services[0].identifier})
identity_token.delete()
return Response(
Expand Down Expand Up @@ -682,23 +686,25 @@ def destroy(self, request, sid=None):
"""
serializer = self.get_serializer(data={"sid": sid})
serializer.is_valid(raise_exception=True)
logger.debug(f"\nDeleting: {sid}")
status = tycho.status({"name": serializer.validated_data["sid"]})
if status.services != None and len(status.services) == 1:
logger.info("service username: " + str(status.services[0].username))
logger.info("request username: " + str(request.user.username))
logger.debug("service username: " + str(status.services[0].username))
logger.debug("request username: " + str(request.user.username))
if status.services[0].username == request.user.username:
logger.info(f"Terminating app id { sid } for user { 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"])
consumer_id = UserIdentityToken.compute_app_consumer_id(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?
time.sleep(2)
return Response(response)
else: return Response(status=drf_status.HTTP_403_FORBIDDEN)
else:
logger.warning(f"User { request.user.username } attempted to terminate app id { sid } owned by user { status.services[0].username }")
return Response(status=drf_status.HTTP_403_FORBIDDEN)
else: return Response(status=drf_status.HTTP_404_NOT_FOUND)

def partial_update(self, request, sid=None):
Expand Down Expand Up @@ -790,7 +796,9 @@ def _get_access_token(self, request):
if request.session.get("Authorization", None):
return request.session["Authorization"].split(" ")[1]
else:
logger.error(f"Authorization not set for {request.user.username}")
# This is not necessarily an error, since authorization (access token)
# may or may not be used, i.e., with authentication via sessionid.
logger.debug(f"Authorization not set for {request.user.username}")
return None

def list(self, request):
Expand Down
15 changes: 14 additions & 1 deletion appstore/appstore/adapter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter

from django.conf import settings
from django.forms import ValidationError

logger = logging.getLogger(__file__)

class RestrictEmailAdapter(DefaultAccountAdapter):
def clean_email(self, email):
Expand Down Expand Up @@ -55,4 +57,15 @@ def get_logout_redirect_url(self, request):
# an error and returning of the route
if request.session.get("helx_frontend"):
del request.session["helx_frontend"]
return url
return url

class SocialAccountAdapter(DefaultSocialAccountAdapter):
def on_authentication_error(self, request, provider, error=None, exception=None, extra_context=None):
provider_id = provider.id if provider else "unknown"
error_code = error.name if error else "unknown"
exception_str = str(exception) if exception else "No exception details"

logger.info(f"User failed to login using allauth:\nprovider id: { provider_id}\nerror code: { error_code }\nexception: { exception_str }")

# Note: this is a no-op, since this hook is unimplemented in the default (super) adapter class.
return super().on_authentication_error(request, provider, error, exception, extra_context)
29 changes: 29 additions & 0 deletions appstore/appstore/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging

""" Used to filter out superfluous logs relating to particular API endpoints. """
class SuperfluousEndpointLogFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
# We do not want to pollute logs with 403s here when all they indicate is that the user is logged out.
if self.is_forbidden_user_list(record):
# Downgrade from a WARNING to a DEBUG. We could also return False to filter it out entirely.
record.levelname = "DEBUG"
record.levelno = 10
return True

""" Does the log originate from a request endpoint?
(This should always be true with proper logging configuration, but worthwhile adding the check.)
"""
def is_request_log(self, record) -> bool:
return hasattr(record, "request") and hasattr(record, "status_code")

"""
A 403 Forbidden is returned by the `GET users` endpoint when a user is logged out/not authenticated.
This response is expected and frequent behavior, since it is the endpoint the frontend uses to
assess authentication status and thus is frequently called.
"""
def is_forbidden_user_list(self, record) -> bool:
return (
self.is_request_log(record) and
record.request.path_info == "/api/v1/users/" and
record.status_code == 403
)
23 changes: 18 additions & 5 deletions appstore/appstore/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
SAML_URL = "/accounts/saml"
SAML_ACS_URL = "/saml2_auth/acs/"
#SAML_ACS_URL = "/sso/acs/"
SOCIALACCOUNT_ADAPATER = "appstore.adapter.SocialAccountAdapter"
SOCIALACCOUNT_QUERY_EMAIL = ACCOUNT_EMAIL_REQUIRED
SOCIALACCOUNT_STORE_TOKENS = True
SOCIALACCOUNT_PROVIDERS = {
Expand Down Expand Up @@ -322,6 +323,12 @@
"level": LOG_LEVEL,
"propagate": False,
},
"django.request": {
"handlers": ["console"],
"level": LOG_LEVEL,
"propagate": False,
"filters": ["skip_superfluous_endpoint_logs"]
},
"django.template": {
"handlers": ["console"],
"level": LOG_LEVEL,
Expand All @@ -336,15 +343,21 @@
"handlers": ["console"],
"level": LOG_LEVEL,
},
"tycho.client": {
"tycho": {
"handlers": ["console"],
"level": LOG_LEVEL,
"level": "WARNING",
},
"tycho.kube": {
# Info logs coming from xmlschema are generally irrelevant and crowd the logs
"xmlschema": {
"handlers": ["console"],
"level": LOG_LEVEL,
},
"level": "WARNING"
}
},
"filters": {
"skip_superfluous_endpoint_logs": {
"()": "appstore.logging.SuperfluousEndpointLogFilter"
}
}
}

csrf_strings = os.environ.get("CSRF_DOMAINS", "")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 4.2 on 2024-10-28 18:45

import core.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0002_auto_20200430_1711'),
]

operations = [
migrations.CreateModel(
name='IrodAuthorizedUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.TextField(max_length=254)),
('uid', models.IntegerField()),
],
),
migrations.AlterField(
model_name='authorizeduser',
name='email',
field=models.EmailField(blank=True, max_length=254),
),
migrations.AlterField(
model_name='authorizeduser',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='authorizeduser',
name='username',
field=models.CharField(blank=True, max_length=128),
),
migrations.CreateModel(
name='UserIdentityToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(default=core.models.generate_token, max_length=256, unique=True)),
('consumer_id', models.CharField(default=None, max_length=256, null=True)),
('expires', models.DateTimeField(default=core.models.user_token_expires)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
4 changes: 2 additions & 2 deletions appstore/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ 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 compute_app_consumer_id(system_id):
return f"{ system_id }"

def __str__(self):
return f"{ self.user.get_username() }-token-{ self.pk }"
Expand Down
21 changes: 21 additions & 0 deletions appstore/core/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import logging
from django.contrib.auth.signals import user_logged_in, user_login_failed, user_logged_out
from django.dispatch import receiver

logger = logging.getLogger("django")

@receiver(user_logged_in)
def on_user_logged_in(sender, request, user, **kwargs):
logger.info(f"User { user.username } logged in")

@receiver(user_logged_out)
def on_user_logged_out(sender, request, user, **kwargs):
# The user object may be None if their session expired prior to logout.
if user: logger.info(f"User { user.username } logged out")
else: logger.info("User logged out (identity unavailable)")

@receiver(user_login_failed)
def on_user_failed_login(sender, credentials, request, **kwargs):
# This will generally only work for form-based login (i.e., not for allauth).
# Allauth failures are logged within the LoginRedirectAdapter `on_authentication_error` hook.
logger.info(f"User failed to login with username { credentials.get('username', '<unknown>') }")
Binary file added appstore/core/static/images/favicon.ico
Binary file not shown.
Loading
Loading