configure for credentials service

This commit is contained in:
lpm0073 2022-05-13 12:56:45 -05:00
parent 6e22e88f9b
commit a61ffe3b29
26 changed files with 385 additions and 30 deletions

View File

@ -31,10 +31,12 @@ setup(
project_urls={
"Code": "https://github.com/lpm0073/tutor-contrib-credentials",
"Issue tracker": "https://github.com/lpm0073/tutor-contrib-credentials/issues",
"Community": "https://discuss.overhang.io",
},
license="AGPLv3",
author="Lawrence McDaniel",
description="credentials plugin for Tutor",
author_email="lpm0073@gmail.com",
description="A Tutor plugin for Open edX Credentials service",
long_description=load_readme(),
packages=find_packages(exclude=["tests*"]),
include_package_data=True,

View File

@ -0,0 +1,7 @@
# credentials service
{{ CREDENTIALS_HOST }}{$default_site_port} {
request_body {
max_size 10MB
}
import proxy "credentials:8000"
}

View File

@ -0,0 +1,37 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: credentials
labels:
app.kubernetes.io/name: credentials
spec:
selector:
matchLabels:
app.kubernetes.io/name: credentials
template:
metadata:
labels:
app.kubernetes.io/name: credentials
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
containers:
- name: credentials
image: {{ CREDENTIALS_DOCKER_IMAGE }}
ports:
- containerPort: 8000
env:
- name: DJANGO_SETTINGS_MODULE
value: credentials.settings.tutor.production
volumeMounts:
- mountPath: /openedx/credentials/credentials/settings/tutor/production.py
name: settings
subPath: production.py
securityContext:
allowPrivilegeEscalation: false
volumes:
- name: settings
configMap:
name: credentials-settings

View File

@ -0,0 +1,25 @@
---
apiVersion: batch/v1
kind: Job
metadata:
name: credentials-job
labels:
app.kubernetes.io/component: job
spec:
template:
spec:
restartPolicy: Never
containers:
- name: credentials
image: {{ CREDENTIALS_DOCKER_IMAGE }}
env:
- name: DJANGO_SETTINGS_MODULE
value: credentials.settings.tutor.production
volumeMounts:
- mountPath: /openedx/credentials/credentials/settings/tutor/production.py
name: settings
subPath: production.py
volumes:
- name: settings
configMap:
name: credentials-settings

View File

@ -0,0 +1,12 @@
---
apiVersion: v1
kind: Service
metadata:
name: credentials
spec:
type: NodePort
ports:
- port: 8000
protocol: TCP
selector:
app.kubernetes.io/name: credentials

View File

@ -0,0 +1,3 @@
- name: credentials-settings
files:
- plugins/credentials/apps/credentials/settings/production.py

View File

@ -0,0 +1 @@
- {{ CREDENTIALS_HOST }}

View File

@ -0,0 +1,15 @@
credentials:
environment:
DJANGO_SETTINGS_MODULE: credentials.settings.tutor.development
command: ./manage.py runserver 0.0.0.0:8150
ports:
- "127.0.0.1:8150:8150"
stdin_open: true
tty: true
volumes:
# editable requirements
- ../plugins/credentials/build/credentials/requirements:/openedx/requirements
networks:
default:
aliases:
- "{{ CREDENTIALS_HOST }}"

View File

@ -0,0 +1,7 @@
credentials-job:
image: {{ CREDENTIALS_DOCKER_IMAGE }}
environment:
DJANGO_SETTINGS_MODULE: credentials.settings.tutor.production
volumes:
- ../plugins/credentials/apps/credentials/settings:/openedx/credentials/credentials/settings/tutor:ro
depends_on: {{ [("mysql", RUN_MYSQL)]|list_if }}

View File

@ -0,0 +1,11 @@
credentials:
image: {{ CREDENTIALS_DOCKER_IMAGE }}
environment:
DJANGO_SETTINGS_MODULE: credentials.settings.tutor.production
restart: unless-stopped
volumes:
- ../plugins/credentials/apps/credentials/settings:/openedx/credentials/credentials/settings/tutor:ro
depends_on:
- discovery
{% if RUN_MYSQL %}- mysql{% endif %}
{% if RUN_LMS %}- lms{% endif %}

View File

@ -0,0 +1 @@
CREDENTIALS_BASE_URL='http://{{ CREDENTIALS_HOST }}:8150'

View File

@ -0,0 +1 @@
CREDENTIALS_BASE_URL='{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ CREDENTIALS_HOST }}'

View File

@ -0,0 +1,5 @@
EDX_API_KEY = "{{ CREDENTIALS_API_KEY }}"
CREDENTIALS_API_SIGNING_KEY = "{{ JWT_COMMON_SECRET_KEY }}"
CREDENTIALS_API_TIMEOUT = {{ CREDENTIALS_API_TIMEOUT }}
EDX_API_KEY = "{{ CREDENTIALS_API_KEY }}"
CREDENTIALS_API_SIGNING_KEY = "{{ JWT_COMMON_SECRET_KEY }}"

View File

@ -0,0 +1,2 @@
CREDENTIALS_PUBLIC_URL_ROOT = "http://{{ CREDENTIALS_HOST }}:8150"
CREDENTIALS_API_URL = CREDENTIALS_PUBLIC_URL_ROOT + "/api/v2"

View File

@ -0,0 +1,2 @@
CREDENTIALS_PUBLIC_URL_ROOT = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ CREDENTIALS_HOST }}"
CREDENTIALS_API_URL = CREDENTIALS_PUBLIC_URL_ROOT + "/api/v2"

View File

@ -2,7 +2,7 @@ from glob import glob
import os
import pkg_resources
from tutor import hooks
from tutor import hooks as tutor_hooks
from .__about__ import __version__
@ -12,6 +12,18 @@ config = {
# Add here your new settings
"defaults": {
"VERSION": __version__,
"API_TIMEOUT": 5,
"CURRENCY": "USD",
"DOCKER_IMAGE": "{{ DOCKER_REGISTRY }}overhangio/openedx-credentials:{{ CREDENTIALS_VERSION }}",
"WORKER_DOCKER_IMAGE": "{{ DOCKER_REGISTRY }}overhangio/openedx-credentials-worker:{{ CREDENTIALS_VERSION }}",
"EXTRA_PIP_REQUIREMENTS": [],
"HOST": "credentials.{{ LMS_HOST }}",
"MYSQL_DATABASE": "credentials",
"MYSQL_USERNAME": "credentials",
"OAUTH2_KEY": "credentials",
"OAUTH2_KEY_DEV": "credentials-dev",
"OAUTH2_KEY_SSO": "credentials-sso",
"OAUTH2_KEY_SSO_DEV": "credentials-sso-dev",
},
# Add here settings that don't have a reasonable default for all users. For
# instance: passwords, secret keys, etc.
@ -25,39 +37,50 @@ config = {
}
################# Initialization tasks
# To run the script from templates/credentials/tasks/myservice/init, add:
# hooks.Filters.COMMANDS_INIT.add_item((
# "myservice",
# ("credentials", "tasks", "myservice", "init"),
# ))
tutor_hooks.Filters.COMMANDS_INIT.add_item(
(
"lms",
("credentials", "tasks", "lms", "init"),
)
)
tutor_hooks.Filters.IMAGES_BUILD.add_item(
(
"credentials",
("plugins", "credentials", "build", "credentials"),
"{{ CREDENTIALS_DOCKER_IMAGE }}",
(),
)
)
################# Docker image management
# To build an image with `tutor images build myimage`, add a Dockerfile to templates/credentials/build/myimage and write:
# hooks.Filters.IMAGES_BUILD.add_item((
# "myimage",
# ("plugins", "credentials", "build", "myimage"),
# "docker.io/myimage:\{\{ CREDENTIALS_VERSION \}\}",
# (),
# )
# To pull/push an image with `tutor images pull myimage` and `tutor images push myimage`, write:
# hooks.Filters.IMAGES_PULL.add_item((
# "myimage",
# "docker.io/myimage:\{\{ CREDENTIALS_VERSION \}\}",
# )
# hooks.Filters.IMAGES_PUSH.add_item((
# "myimage",
# "docker.io/myimage:\{\{ CREDENTIALS_VERSION \}\}",
# )
@tutor_hooks.Filters.IMAGES_PULL.add()
@tutor_hooks.Filters.IMAGES_PUSH.add()
def _add_remote_credentials_image_iff_customized(images, user_config):
"""
Register CREDENTIALS image for pushing & pulling if and only if it has
been set to something other than the default.
This is work-around to an upstream issue with CREDENTIALS config. Briefly:
User config is baked into CREDENTIALS builds, so Tutor cannot host a generic
pre-built CREDENTIALS image. Howevever, individual Tutor users may want/need to
build and host their own CREDENTIALS image. So, as a compromise, we tell Tutor
to push/pull the CREDENTIALS image if the user has customized it to anything
other than the default image URL.
"""
image_tag = user_config["CREDENTIALS_DOCKER_IMAGE"]
if not image_tag.startswith("docker.io/lpm0073/openedx-credentials:"):
# Image has been customized. Add to list for pulling/pushing.
images.append(("credentials", image_tag))
return images
################# You don't really have to bother about what's below this line,
################# except maybe for educational purposes :)
# Plugin templates
hooks.Filters.ENV_TEMPLATE_ROOTS.add_item(
tutor_hooks.Filters.ENV_TEMPLATE_ROOTS.add_item(
pkg_resources.resource_filename("tutorcredentials", "templates")
)
hooks.Filters.ENV_TEMPLATE_TARGETS.add_items(
tutor_hooks.Filters.ENV_TEMPLATE_TARGETS.add_items(
[
("credentials/build", "plugins"),
("credentials/apps", "plugins"),
@ -71,19 +94,19 @@ for path in glob(
)
):
with open(path, encoding="utf-8") as patch_file:
hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))
tutor_hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))
# Load all configuration entries
hooks.Filters.CONFIG_DEFAULTS.add_items(
tutor_hooks.Filters.CONFIG_DEFAULTS.add_items(
[
(f"CREDENTIALS_{key}", value)
for key, value in config["defaults"].items()
]
)
hooks.Filters.CONFIG_UNIQUE.add_items(
tutor_hooks.Filters.CONFIG_UNIQUE.add_items(
[
(f"CREDENTIALS_{key}", value)
for key, value in config["unique"].items()
]
)
hooks.Filters.CONFIG_OVERRIDES.add_items(list(config["overrides"].items()))
tutor_hooks.Filters.CONFIG_OVERRIDES.add_items(list(config["overrides"].items()))

View File

@ -0,0 +1,14 @@
from ..devstack import *
{% include "credentials/apps/credentials/settings/partials/common.py" %}
CORS_ORIGIN_WHITELIST = list(CORS_ORIGIN_WHITELIST) + [
"http://{{ MFE_HOST }}:{{ CREDENTIALS_MFE_APP['port'] }}",
]
CSRF_TRUSTED_ORIGINS = ["{{ MFE_HOST }}:{{ CREDENTIALS_MFE_APP['port'] }}"]
SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT = "http://{{ LMS_HOST }}:8000"
BACKEND_SERVICE_EDX_OAUTH2_KEY = "{{ CREDENTIALS_OAUTH2_KEY_DEV }}"
{{ patch("credentials-settings-development") }}

View File

@ -0,0 +1,82 @@
import json
SECRET_KEY = "{{ CREDENTIALS_SECRET_KEY }}"
ALLOWED_HOSTS = [
"{{ CREDENTIALS_HOST }}",
"credentials",
]
PLATFORM_NAME = "{{ PLATFORM_NAME }}"
PROTOCOL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}"
CORS_ALLOW_CREDENTIALS = True
OSCAR_DEFAULT_CURRENCY = "{{ CREDENTIALS_CURRENCY }}"
EDX_API_KEY = "{{ CREDENTIALS_API_KEY }}"
{% set jwt_rsa_key = rsa_import_key(JWT_RSA_PRIVATE_KEY) %}
JWT_AUTH["JWT_ISSUER"] = "{{ JWT_COMMON_ISSUER }}"
JWT_AUTH["JWT_AUDIENCE"] = "{{ JWT_COMMON_AUDIENCE }}"
JWT_AUTH["JWT_SECRET_KEY"] = "{{ JWT_COMMON_SECRET_KEY }}"
JWT_AUTH["JWT_PUBLIC_SIGNING_JWK_SET"] = json.dumps(
{
"keys": [
{
"kid": "openedx",
"kty": "RSA",
"e": "{{ jwt_rsa_key.e|long_to_base64 }}",
"n": "{{ jwt_rsa_key.n|long_to_base64 }}",
}
]
}
)
JWT_AUTH["JWT_ISSUERS"] = [
{
"ISSUER": "{{ JWT_COMMON_ISSUER }}",
"AUDIENCE": "{{ JWT_COMMON_AUDIENCE }}",
"SECRET_KEY": "{{ OPENEDX_SECRET_KEY }}"
}
]
SOCIAL_AUTH_REDIRECT_IS_HTTPS = {% if ENABLE_HTTPS %}True{% else %}False{% endif %}
SOCIAL_AUTH_EDX_OAUTH2_ISSUER = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}"
SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = "http://lms:8000"
BACKEND_SERVICE_EDX_OAUTH2_SECRET = "{{ CREDENTIALS_OAUTH2_SECRET }}"
BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL = "http://lms:8000/oauth2"
EDX_DRF_EXTENSIONS = {
'OAUTH2_USER_INFO_URL': '{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/oauth2/user_info',
}
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "{{ CREDENTIALS_MYSQL_DATABASE }}",
"USER": "{{ CREDENTIALS_MYSQL_USERNAME }}",
"PASSWORD": "{{ CREDENTIALS_MYSQL_PASSWORD }}",
"HOST": "{{ MYSQL_HOST }}",
"PORT": "{{ MYSQL_PORT }}",
"OPTIONS": {
"init_command": "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "{{ SMTP_HOST }}"
EMAIL_PORT = "{{ SMTP_PORT }}"
EMAIL_HOST_USER = "{{ SMTP_USERNAME }}"
EMAIL_HOST_PASSWORD = "{{ SMTP_PASSWORD }}"
EMAIL_USE_TLS = {{SMTP_USE_TLS}}
ENTERPRISE_SERVICE_URL = '{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/enterprise/'
ENTERPRISE_API_URL = urljoin(ENTERPRISE_SERVICE_URL, 'api/v1/')
# Get rid of local logger
LOGGING["handlers"].pop("local")
for logger in LOGGING["loggers"].values():
logger["handlers"].remove("local")
{{ patch("credentials-settings-common") }}

View File

@ -0,0 +1,14 @@
from ..production import *
{% include "credentials/apps/credentials/settings/partials/common.py" %}
CORS_ORIGIN_WHITELIST = list(CORS_ORIGIN_WHITELIST) + [
"{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ MFE_HOST }}",
]
CSRF_TRUSTED_ORIGINS = ["{{ MFE_HOST }}"]
SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}"
BACKEND_SERVICE_EDX_OAUTH2_KEY = "{{ CREDENTIALS_OAUTH2_KEY }}"
{{ patch("credentials-settings-production") }}

View File

@ -0,0 +1,71 @@
FROM docker.io/ubuntu:20.04
RUN apt update && \
apt install -y curl git-core language-pack-en libmysqlclient-dev libssl-dev python3 python3-pip python3-venv
ARG APP_USER_ID=1000
RUN useradd --home-dir /openedx --create-home --shell /bin/bash --uid ${APP_USER_ID} app
USER ${APP_USER_ID}
# Create python venv
RUN python3 -m venv /openedx/venv/
ENV PATH "/openedx/venv/bin:$PATH"
RUN pip install setuptools==44.1.0 pip==20.3.4 wheel==0.37.0
# Install a recent version of nodejs
RUN pip install nodeenv
RUN nodeenv /openedx/nodeenv --node=12.13.0 --prebuilt
ENV PATH /openedx/nodeenv/bin:${PATH}
# Install credentials
ARG CREDENTIALS_REPOSITORY=https://github.com/edx/credentials.git
ARG CREDENTIALS_VERSION={{ OPENEDX_COMMON_VERSION }}
RUN mkdir -p /openedx/credentials && \
git clone $CREDENTIALS_REPOSITORY --branch $CREDENTIALS_VERSION --depth 1 /openedx/credentials
WORKDIR /openedx/credentials
# Identify tutor user to cherry-pick commits
RUN git config --global user.email "tutor@overhang.io" \
&& git config --global user.name "Tutor"
# nodejs requirements (aka: "make requirements.js")
ARG NPM_REGISTRY=https://registry.npmjs.org/
RUN npm install --verbose --registry=$NPM_REGISTRY
RUN ./node_modules/.bin/bower install --allow-root
# python requirements
RUN pip install -r requirements.txt
RUN pip install uwsgi==2.0.19.1
# Install private requirements
COPY --chown=app:app ./requirements/ /openedx/requirements
RUN cd /openedx/requirements/ \
&& touch ./private.txt \
&& pip install -r ./private.txt
{% for extra_requirement in CREDENTIALS_EXTRA_PIP_REQUIREMENTS %}RUN pip install {{ extra_requirement }}
{% endfor %}
# Collect static assets (aka: "make static")
COPY --chown=app:app assets.py ./credentials/settings/assets.py
ENV DJANGO_SETTINGS_MODULE credentials.settings.assets
RUN python3 manage.py update_assets --skip-collect
RUN ./node_modules/.bin/r.js -o build.js
RUN python3 manage.py collectstatic --noinput
RUN python3 manage.py compress --force
# Setup minimal yml config file, which is required by production settings
RUN echo "{}" > /openedx/config.yml
ENV CREDENTIALS_CFG /openedx/config.yml
EXPOSE 8000
CMD uwsgi \
--static-map /static=/openedx/credentials/assets \
--static-map /media=/openedx/credentials/course_credentials/media \
--http 0.0.0.0:8000 \
--thunder-lock \
--single-interpreter \
--enable-threads \
--processes=2 \
--buffer-size=8192 \
--wsgi-file credentials/wsgi.py

View File

@ -0,0 +1,9 @@
from .base import *
# Get rid of local logger
LOGGING["handlers"].pop("local")
for logger in LOGGING["loggers"].values():
logger["handlers"].remove("local")
COMPRESS_ENABLED = True
COMPRESS_OFFLINE = True

View File

@ -0,0 +1,6 @@
# Add your additional requirements, such as payment processors, to this file. For
# requirements coming from private repositories, clone the repository to this
# folder and then add your requirement with the `-e` flag. Ex:
#
# git clone git@myserver:myprivaterepo.git
# echo "-e ./myprivaterepo/" >> private.txt

View File

@ -0,0 +1 @@
./manage.py migrate --noinput

View File

@ -0,0 +1,4 @@
mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e 'CREATE DATABASE IF NOT EXISTS {{ CREDENTIALS_MYSQL_DATABASE }};'
mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e "CREATE USER IF NOT EXISTS '{{ CREDENTIALS_MYSQL_USERNAME }}';"
mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e "ALTER USER '{{ CREDENTIALS_MYSQL_USERNAME }}'@'%' IDENTIFIED BY '{{ CREDENTIALS_MYSQL_PASSWORD }}';"
mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e "GRANT ALL ON {{ CREDENTIALS_MYSQL_DATABASE }}.* TO '{{ CREDENTIALS_MYSQL_USERNAME }}'@'%';"