From 854b7ab9be83c44f4ded920d7509cc43ca155b1b Mon Sep 17 00:00:00 2001 From: vsoch Date: Sun, 9 Oct 2022 16:38:12 -0600 Subject: [PATCH] consolidate secrets into same settings the user can now define variables directly in settings.py, via an external secrets.py or settings.yaml, or in the environment with SREGISTRY_ as a prefix. Signed-off-by: vsoch --- CHANGELOG.md | 3 +- VERSION | 2 +- docs/_docs/install/settings.md | 138 ++++++++--------- requirements.txt | 8 +- shub/dummy-settings.yaml | 226 +++++++++++++++++++++++++++ shub/dummy_secrets.py | 16 +- shub/plugins/google_build/utils.py | 4 +- shub/settings.py | 235 ++++++++++++++++++++++------- 8 files changed, 482 insertions(+), 150 deletions(-) create mode 100644 shub/dummy-settings.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 287e63f7..5ffd2cc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ represented by the pull requests that fixed them. Critical items to know are: ## [master](https://github.com/singularityhub/sregistry/tree/master) (master) - - consolidate config into one file with environment (1.1.41) + - consolidate config into one file with environment (2.0.0) + - This is an API breaking change, as the settings are completely refactored - update python base to 3.9, minio server to use new credentials (1.1.4) - docker-compose updated to use docker compose - add: auto set "verify" attribute of s3 and s3_external obj in minio.py for SSL use (1.1.39) diff --git a/VERSION b/VERSION index a2f3bf51..227cea21 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.41 +2.0.0 diff --git a/docs/_docs/install/settings.md b/docs/_docs/install/settings.md index 3c93e33a..87fc9db8 100644 --- a/docs/_docs/install/settings.md +++ b/docs/_docs/install/settings.md @@ -6,20 +6,33 @@ toc: false # Settings -As of version `1.1.41` we have one core [settings.py](https://github.com/singularityhub/sregistry/blob/master/shub/settings.py) file +As of version `2.0.0` we have one core [settings.py](https://github.com/singularityhub/sregistry/blob/master/shub/settings.py) file that can be populated in several ways: - Directly in the file (e.g., if you build your own base container) - From the environment (described below with a `SREGISTRY_` prefix - From a config file, `settings.yaml` in the root directory of your server OR custom set at `SREGISTRY_SETTINGS_FILE` + +Order of preference or variables honored is: + + 1. secrets.py + 2. the environment + 3. settings.yaml + 4. defaults directly in settings.py + +E.g., anything you define in a secrets.py takes first preference, followed by the environment, then a settings.yaml, and lastly, the defaults directly in settings.py. You will want to choose a strategy that works for your deployment, and then tweak the values to your liking before we start the application. -For example, if you are running sregistry on a filesystem directly that you can access, you can easily write secrets into a `secrets.py` in the -`shub/` directory alongside settings and you can use the [dummy_secrets.py](https://github.com/singularityhub/sregistry/blob/master/shub/dummy_secrets.py) as a starter template. However, if you are deploying via Kubernetes or similar, you can either choose to define secrets entirely in the environment, or you can have a `settings.yaml` that is created in a manifest and passed to the application. +For example, if you are running sregistry on a filesystem directly that you can access, you can easily write secrets into _either_: + + - A `secrets.py` in the `shub/` directory alongside settings and you can use the [dummy_secrets.py](https://github.com/singularityhub/sregistry/blob/master/shub/dummy_secrets.py) as a starter template. + - A `settings.yaml` in the root directory of your application (or set at `SREGISTRY_SETTINGS_FILE` and you can use the [dummy-settings.yaml](https://github.com/singularityhub/sregistry/blob/master/shub/dummy-settings.yaml) as a starter template. + +If you are deploying via Kubernetes or similar, you can either choose to define secrets entirely in the environment, or you can have one of these files added via a config map or similar (or some combination of those two things). ## Environment -As of version `1.1.41`, you can set any configuration value from settings in the environment to override the settings value. +As of version `2.0.0`, you can set any configuration value from settings in the environment to override the settings value. To determine the value for the environment variable, if it isn't already defined in the environment (e.g., `MINIO_SECRET`) you can add the `SREGISTRY_` prefix to derive it. This means: @@ -38,97 +51,74 @@ are exposed (all strings): - SREGISTRY_DATABASE_HOST (db) - SREGISTRY_DATABASE_PORT (5432) -And you can set a custom set of comma separated values (in a string) for either of `SREGISTRY_ADMINS` or `SREGISTRY_PLUGINS_ENABLED`. +And you can set a custom set of _comma separated values_ (in a string) for either of `SREGISTRY_ADMINS` or `SREGISTRY_PLUGINS_ENABLED`. ```console SREGISTRY_ADMINS="adminA,adminB,adminC" SREGISTRY_PLUGINS_ENABLED="ldap,google-build" ``` -Finally, if you need to set secrets from the environment (and don't want to provide the secrets.py file) you can -do the same by taking any secret value and prefixing it with `SREGISTRY_SECRET_`. For this approach, we currently -support strings and booleans (true/false derivatives) and we can add support for other types if requested. - -For example, to set your secret key (required) you would do: - -```console -SREGISTRY_SECRET_SECRET_KEY = 'xxxxxxxxxxxxxxxxxx' -``` -For any setting in the sections below, you can set them in the environment (with `SREGISTRY_` or `SREGISTRY_SECRET` prefix for secrets) -or via the settings.yaml file. Each of these settings is explained in more detail in the sections below. - -## Secrets - -There should be a file called `secrets.py` in the shub settings folder (it won't exist in the repo, you have to make it), in which you will store the application secret and other social login credentials. - -An template to work from is provided in the settings folder called `dummy_secrets.py`. You can copy this template: +For any setting in the sections below, you can set them in the environment (with `SREGISTRY_`) +or via the settings.yaml file or the `secrets.py`. If you choose to use a file (and are working locally) you'll want to start by copying +either of the templates: ```bash cp shub/dummy_secrets.py shub/secrets.py +cp shub/dummy-settings.yaml settings.yaml ``` +And make sure neither of these files are added to any kind of version control! +Each of the settings (optional or required) is explained in more detail in the sections below. -Or, if you prefer a clean secrets file, create a blank one as below: - -```bash -touch shub/secrets.py -``` - -and inside you want to add a `SECRET_KEY`. You can use the [secret key generator](http://www.miniwebtool.com/django-secret-key-generator/) to make a new secret key, and call it `SECRET_KEY` in your `secrets.py` file, like this: - -``` -SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' -``` - -And you can also set this in the environment: +## Secrets -```bash -export SREGISTRY_SECRET_SECRET_KEY=xxxxxxxxxxxxxxxxxx -``` +Regardless of your approach, you'll want to set a `SECRET_KEY` - either directly in `secrets.py` or `settings.yaml` or +in the environment as `SREGISTRY_SECRET_KEY`. +You can use the [secret key generator](http://www.miniwebtool.com/django-secret-key-generator/) to make a new secret key ### Authentication Secrets -One thing I (@vsoch) can't do for you in advance is produce application keys and secrets to give your Registry for each social provider that you want to allow users (and yourself) to login with. We are going to use a framework called [python social auth](https://python-social-auth-docs.readthedocs.io/en/latest/configuration/django.html) to achieve this, and in fact you can add a [number of providers](http://python-social-auth-docs.readthedocs.io/en/latest/backends/index.html) (I have set up a lot of them, including SAML, so please submit an issue if you want one added to the base proper.). Singularity Registry uses OAuth2 with a token--> refresh flow because it gives the user power to revoke permission at any point, and is a more modern strategy than storing a database of usernames and passwords. You can enable or disable as many of these that you want, and this is done in the [settings.py](https://github.com/singularityhub/sregistry/blob/master/shub/settings.py), which is controllable via the environment or a `settings.yaml` or by editing the file directly: +One thing I (@vsoch) can't do for you in advance is produce application keys and secrets to give your Registry for each social provider that you want to allow users (and yourself) to login with. We are going to use a framework called [python social auth](https://python-social-auth-docs.readthedocs.io/en/latest/configuration/django.html) to achieve this, and in fact you can add a [number of providers](http://python-social-auth-docs.readthedocs.io/en/latest/backends/index.html) (I have set up a lot of them, including SAML, so please submit an issue if you want one added to the base proper.). Singularity Registry uses OAuth2 with a token--> refresh flow because it gives the user power to revoke permission at any point, and is a more modern strategy than storing a database of usernames and passwords. You can enable or disable as many of these that you want, and this is done in the [settings.py](https://github.com/singularityhub/sregistry/blob/master/shub/settings.py), which is controllable via the environment or a `settings.yaml` or by editing the file directly. For example, if using the `settings.yaml` you will want to have at least one of these enabled: -```python -# Which social auths do you want to use? -ENABLE_GOOGLE_AUTH=False -ENABLE_TWITTER_AUTH=False -ENABLE_GITHUB_AUTH=True -ENABLE_GITLAB_AUTH=False -ENABLE_BITBUCKET_AUTH=False -ENABLE_GITHUB_ENTERPRISE_AUTH=False +```yaml +API_REQUIRE_AUTH: false +ENABLE_GOOGLE_AUTH: false +ENABLE_TWITTER_AUTH: false +ENABLE_GITHUB_AUTH: true +ENABLE_GITLAB_AUTH: false +ENABLE_BITBUCKET_AUTH: false +ENABLE_GITHUB_ENTERPRISE_AUTH: false ``` -Any of the above can be set in the environment with the `SREGISTRY_` prefix. -You will need at least one to log in. I've found that GitHub works the fastest and easiest, and then Google. -Twitter now requires an actual server name and won't work with localhost, but if you are deploying on a server with a proper domain go ahead and use it. All avenues are extremely specific with regard to callback urls, so you should be very careful in setting them up. If you want automated builds from a repository -integration with Google Cloud Build, then you must use GitHub. +Note that any of the above can also be set in the environment with the `SREGISTRY_` prefix, or you can define Python boolean +values in a `secrets.py`. You will need at least one to log in. I've found that GitHub works the fastest and easiest, and then Google. +Twitter now requires an actual server name and won't work with localhost, but if you are deploying on a server with a proper domain go ahead and use it. All avenues are extremely specific with regard to callback urls, so you should be very careful in setting them up. If you want automated builds from a repository integration with Google Cloud Build, then you must use GitHub. ## Plugins Other authentication methods, such as LDAP, are implemented as [plugins](https://singularityhub.github.io/sregistry/docs/plugins/) to sregistry. See the [plugins documentation](https://singularityhub.github.io/sregistry/docs/plugins/) for details on how to configure these. You should also now look here to see which plugins you will -want to set up (and then build into your container). +want to set up (and then build into your container). You can look at either of the dummy `secrets.py` or `settings.yaml` to see the variables that are required (and examples). For authentication plugins, we will walk through the setup of each in detail here. -For other plugins, you should look at the [plugins](https://singularityhub.github.io/sregistry/docs/plugins/) documentation now before proceeding. For all of the below, you should put the content in your `secrets.py` under settings. Note that if you are deploying locally, you will need to put localhost (127.0.0.1) as your domain, and Github is now the only one that worked reliably without an actual domain for me. +For other plugins, you should look at the [plugins](https://singularityhub.github.io/sregistry/docs/plugins/) documentation now before proceeding. + +Remember, for all of the below, you should put the content in one of: + +1. your `secrets.py` +2. your `settings.yaml` +3. the environment with a `SREGISTRY_` prefix +4. directly in the file (be careful about adding this to version control!) + +Note that if you are deploying locally, you will need to put localhost (127.0.0.1) as your domain, and Github is now the only one that worked reliably without an actual domain for me. For the remainder of this settings document, we will provide examples written to `secrets.py`. ### Google OAuth2 You first need to [follow the instructions](https://developers.google.com/identity/protocols/OpenIDConnect) and setup an OAuth2 API credential. The redirect URL should be every variation of having http/https, and www. and not. (Eg, change around http-->https and with and without www.) of `https://www.sregistry.org/complete/google-oauth2/`. Google has good enough debugging that if you get this wrong, it will give you an error message with what is going wrong. You should store the credential in `secrets.py`, along with the complete path to the file for your application: ```python -GOOGLE_CLIENT_FILE='/code/.grilledcheese.json' - # http://psa.matiasaguirre.net/docs/backends/google.html?highlight=google SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'xxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com' SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'xxxxxxxxxxxxxxxxx' -# The scope is not needed, unless you want to develop something new. -#SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['https://www.googleapis.com/auth/drive'] -SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = { - 'access_type': 'offline', - 'approval_prompt': 'auto' -} ``` Google is great in letting you specify multiple acceptable callback urls, so you should set every version of `http://127.0.0.1/complete/google-oauth2` (I did with and without http/https, along with the ending and without the ending slash, just in case). Note that `1.` extra arguments have been added to ensure that users can refresh tokens, and `2.` in testing I was using `http` and not `https`, and I eventually added `https` (and so the url was adjusted accordingly). Next, we need to follow instructions for [web applications](https://developers.google.com/identity/protocols/OAuth2WebServer). @@ -142,14 +132,6 @@ For users to connect to Github, you need to [register a new application](https:/ # http://psa.matiasaguirre.net/docs/backends/github.html?highlight=github SOCIAL_AUTH_GITHUB_KEY = '' SOCIAL_AUTH_GITHUB_SECRET = '' - -# If you want to use the google_build plugin, you will need to include the following: -SOCIAL_AUTH_GITHUB_SCOPE = ["admin:repo_hook", - "repo:status", - "user:email", - "read:org", - "admin:org_hook", - "deployment_status"] ``` The callback url should be in the format `http://127.0.0.1/complete/github`, and replace the localhost address with your domain. See the [Github Developers](https://github.com/settings/developers) pages to browse more information on the Github APIs. @@ -180,7 +162,6 @@ Instructions are provided [here](https://github.com/python-social-auth/social-do 3. In your `secrets.py` file under settings, add: ``` -SOCIAL_AUTH_GITLAB_SCOPE = ['api', 'read_user'] SOCIAL_AUTH_GITLAB_KEY = '' SOCIAL_AUTH_GITLAB_SECRET = '' ``` @@ -226,6 +207,7 @@ ENABLE_BITBUCKET_AUTH=True ``` ### Setting up Twitter OAuth2 + You can go to the [Twitter Apps](https://apps.twitter.com/) dashboard, register an app, and add secrets, etc. to your `secrets.py`: ```bash @@ -407,6 +389,15 @@ DATABASES = { } ``` +And remember you can set these in the environment too: + + - SREGISTRY_DATABASE_ENGINE + - SREGISTRY_DATABASE_NAME (postgres) + - SREGISTRY_DATABASE_USER (postgres) + - SREGISTRY_DATABASE_HOST (db) + - SREGISTRY_DATABASE_PORT (5432) + + ### Logging By default, Singularity Registry keeps track of all requests to pull containers, and you have control over the level of detail that is kept. If you want to save complete metadata (meaning the full json response for each call) then you should set `LOGGING_SAVE_RESPONSES` to True. If you expect heavy use and want to save the minimal (keep track of which collections are pulled how many times) the reccomendation is to set this to False. @@ -425,12 +416,5 @@ To configure your restful API you can set `SREGISTRY_API_REQUIRE_AUTH` to true. ), ``` -And also choose throttle rates for users and anonymous API requests -with the following: - - - These are important metrics to ensure that your server isn't subject to a DoS attack. - - Great job! Let's now [configure your web server and storage](server). diff --git a/requirements.txt b/requirements.txt index b5dd3696..0f6a0f45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ coreapi==2.3.3 cython +django==2.2.28 django-chosen django-crispy-forms django-datatables-view @@ -12,18 +13,18 @@ django-guardian django-hstore==1.3.5 django-notifications-hq django-ratelimit==2.0.0 +djangorestframework==3.11.2 django-rest-swagger django-rq django-taggit django-taggit-templatetags django-user-agents -django==2.2.28 -djangorestframework==3.11.2 google google-api-python-client h5py ipython markdown +minio==5.0.8 numexpr oauth2client==3.0 Pillow @@ -31,6 +32,7 @@ psycopg2-binary~=2.8.6 pygments python3-saml python-social-auth +pyyaml requests requests-oauthlib requests-toolbelt @@ -41,5 +43,3 @@ social-auth-app-django social-auth-core[saml] sregistry[all-basic]>=0.2.19 uwsgi -minio==5.0.8 -pyyaml diff --git a/shub/dummy-settings.yaml b/shub/dummy-settings.yaml new file mode 100644 index 00000000..7d629bdb --- /dev/null +++ b/shub/dummy-settings.yaml @@ -0,0 +1,226 @@ +# You can rename this file to settings.yaml (and customize) to define your settings +# As a reminder, you can define settings here, in a secrets.py, or the environment. +# Any of the below can be set in the environment by adding the SREGISTRY_ prefix + +# SERVER + +# YOU MUST DEFINE THIS +# e.g. use a generator https://djskgen.herokuapp.com/ +# SECRET_KEY=xxxxxxxxxxxxxxxxxxxx + +# Domains + +## IMPORTANT: if/when you switch to https, you need to change DOMAIN_NAME +# to have https, otherwise some functionality will not work (e.g., GitHub webhooks) +DOMAIN_NAME: http://127.0.0.1 +DOMAIN_NAME_HTTP: http://127.0.0.1 + +# Get additional admins from the environment +HELP_CONTACT_EMAIL: vsoch@users.noreply.github.com +HELP_INSTITUTION_SITE: https://srcc.stanford.edu +REGISTRY_NAME: Tacosaurus Computing Center +REGISTRY_URI: taco +# An image url or one of the following: 'mm', 'identicon', 'monsterid', 'wavatar', 'retro'. Defaults to 'mm', and 'retro' here +GRAVATAR_DEFAULT_IMAGE: retro +# GOOGLE_ANALYTICS: UA-XXXXXXXXX + +# DATABASE + +DATABASE_ENGINE: django.db.backends.postgresql_psycopg2 +DATABASE_NAME: postgres +DATABASE_USER: postgres +DATABASE_HOST: db +DATABASE_PORT: "5432" + +# STORAGE + +# Minio Storage + +# use SSL for minio +MINIO_SSL: false +MINIO_MULTIPART_UPLOAD: true +# Don't clean up images in Minio that are no longer referenced by sregistry +DISABLE_MINIO_CLEANUP: false +MINIO_ROOT_USER: null +MINIO_ROOT_PASSWORD: null +MINIO_SERVER: minio:9000 # Internal to sregistry +MINIO_EXTERNAL_SERVER: 127.0.0.1:9000 # minio server for Singularity to interact with +MINIO_BUCKET: sregistry +MINIO_REGION: us-east-1 +MINIO_SIGNED_URL_EXPIRE_MINUTES: 5 + +# Redis + +REDIS_HOST: redis +REDIS_URL: redis://redis/0 + +# SOCIAL AUTH + +# Which social auths do you want to use (you must choose one)? +API_REQUIRE_AUTH: false +ENABLE_GOOGLE_AUTH: false +ENABLE_TWITTER_AUTH: false +ENABLE_GITHUB_AUTH: true +ENABLE_GITLAB_AUTH: false +ENABLE_BITBUCKET_AUTH: false +ENABLE_GITHUB_ENTERPRISE_AUTH: false + +# NOTE you will need to set authentication methods up for your choice above. +# See https://singularityhub.github.io/sregistry/docs/install/settings +# The commented out variables to set are provided below! + +SOCIAL_AUTH_LOGIN_REDIRECT_URL: http://127.0.0.1 +# On any admin or plugin login redirect to standard social-auth entry point for agreement to terms +LOGIN_REDIRECT_URL: /login + +# Twitter Social Authentication +# SOCIAL_AUTH_TWITTER_KEY: xxxxxxxx +# SOCIAL_AUTH_TWITTER_SECRET: xxxxxxxxxxxx + +# Google social authentication +# http://psa.matiasaguirre.net/docs/backends/google.html?highlight=google +# SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: xxxxxxxxxxxxxxxxxx.apps.googleusercontent.com +# SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: xxxxxxxxxxxxxxxxxxxxx + +# GitHub social authentication +# http://psa.matiasaguirre.net/docs/backends/github.html?highlight=github +# SOCIAL_AUTH_GITHUB_KEY: xxxxxxxxxxxx +# SOCIAL_AUTH_GITHUB_SECRET: xxxxxxxxxxxx + +# GitHub Enterprise +# SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: xxxxxxxxxxxx +# Set the API URL for your GitHub Enterprise appliance: +# SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: xxxxxxxxxxxx +# Fill the Client ID and Client Secret values from GitHub in the settings +# SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: xxxxxxxxxxxx +# SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: xxxxxxxxxxxx + +# GitLab OAuth2 +# SOCIAL_AUTH_GITLAB_KEY: xxxxxxxxxxxx +# SOCIAL_AUTH_GITLAB_SECRET: xxxxxxxxxxxx + +# Bitbucket social auth +# SOCIAL_AUTH_BITBUCKET_OAUTH2_KEY: '' +# SOCIAL_AUTH_BITBUCKET_OAUTH2_SECRET: '' +# If using bitbucket, only allow verified emails +# SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY: True + +# COLLECTIONS AND USER PERMISSIONS/LIMITS + +# Allow users to create public collections +USER_COLLECTIONS: true +# Should registries by default be private, with no option for public? +PRIVATE_ONLY: false +# Should the default for a new registry be private or public? +DEFAULT_PRIVATE: false +# Disable all pushes of containers, recipes, etc. Also for Google Cloud Build +DISABLE_BUILDING: false +# A global setting to disable all webhooks / interaction with Github +DISABLE_GITHUB: false +# prevent responses from being received from Google Cloud Build +DISABLE_BUILD_RECEIVE: false +# Set this to be some size in MB to limit uploads. +# Uploads > 2.5GB will not use memory, but the filesystem +# DATA_UPLOAD_MAX_MEMORY_SIZE: +# Limit users to N collections (None is unlimited) +# USER_COLLECTION_LIMIT: 2 +# The number of collections to show on the //collections page +COLLECTIONS_VIEW_PAGE_COUNT: 250 +# The maximum number of downloads allowed per container/collection, per week +CONTAINER_WEEKLY_GET_LIMIT: 100 +COLLECTION_WEEKLY_GET_LIMIT: 100 + +# LOGGING + +# Do you want to save complete response metadata per each pull? +# If you disable, we still keep track of collection pull counts, but not specific versions +LOGGING_SAVE_RESPONSES: true +DJANGO_LOG_LEVEL: WARNING + +# RATE LIMITING + +# Given that someone goes over, are they blocked for the period? +VIEW_RATE_LIMIT_BLOCK: true +# The rate limit for each view, django-ratelimit, 50 per day per ipaddress) +VIEW_RATE_LIMIT: 50/1d + +# API SETTINGS + +API_VERSION: v1 +API_ANON_THROTTLE_RATE: 100/day +API_USER_THROTTLE_RATE: 1000/day +API_DEFAULT_SCHEMA_CLASS: rest_framework.schemas.coreapi.AutoSchema +API_DEFAULT_PAGINATION_CLASS: rest_framework.pagination.LimitOffsetPagination +API_PAGE_SIZE: 10 + +# PLUGINS + +# Google Cloud Build + Storage: configure a custom builder and storage endpoint + +# GOOGLE_BUILD_SINGULARITY_VERSION: v3.3.0-slim +# GOOGLE_APPLICATION_CREDENTIALS: /path/to/credentials.json" +# GOOGLE_PROJECT: "myproject", +# After build, do not delete intermediate dependencies in cloudbuild bucket (keep them as cache for rebuild if needed). +# Defaults to being unset, meaning that files are cleaned up. If you define this as anything, the build files will be cached. +# GOOGLE_BUILD_CACHE: "true" +# if you want to specify a version of Singularity. The version must coincide with a container tag hosted under singularityware/singularity. +# The version will default to 3.2.0-slim If you want to use a different version, update this variable. +# GOOGLE_BUILD_SINGULARITY_VERSION: v3.2.1-slim +# GOOGLE_STORAGE_BUCKET: taco-singularity-registry +# the name for the bucket you want to create. The example here is using the unique identifier appended with “sregistry- +# If you don't define it, it will default to a string that includes the hostname. +# Additionally, a temporary bucket is created with the same name ending in _cloudbuild. This bucket is for build time dependencies, +# and is cleaned up after the fact. If you are having trouble getting a bucket it is likely because the name is taken, +# and we recommend creating both and _cloudbuild in the console and then setting the name here. +# GOOGLE_BUILD_LIMIT: 100 +# GOOGLE_BUILD_TIMEOUT_SECONDS: # unset defaults to 10 minutes +# GOOGLE_BUILD_EXPIRE_SECONDS: 28800 +# Google Build +# To prevent denial of service attacks on Google Cloud Storage, you should set a reasonable limit for the number of active, concurrent builds. +# This number should be based on your expected number of users, repositories, and recipes per repository. +# GOOGLE_BUILD_LIMIT: 100 +# The number of seconds for the build to timeout. If set to None, will be 10 minutes. If +# unset, will default to 3 hours. This time should be less than the GOOGLE_BUILD_EXPIRE_SECONDS +# GOOGLE_BUILD_TIMEOUT_SECONDS: +# The number of seconds for the build to expire, meaning it's response is no longer accepted by the server. This must be defined. +# Using 28800 would indicate 8 hours (in seconds) +# GOOGLE_BUILD_EXPIRE_SECONDS: 28800 +# The number of seconds to expire a signed URL given to download a container +# from storage. This can be much smaller than 10, as we only need it to endure +# for the POST. +# CONTAINER_SIGNED_URL_EXPIRE_SECONDS: 10 + + +# LDAP Authentication (ldap-auth) + +# Only required if 'ldap-auth' is added to PLUGINS_ENABLED +# This example assumes you are using an OpenLDAP directory +# If using an alternative directory - e.g. Microsoft AD, 389 you +# will need to modify attribute names/mappings accordingly +# See https://django-auth-ldap.readthedocs.io/en/1.2.x/index.html + +# The URI to our LDAP server (may be ldap:// or ldaps://) +# AUTH_LDAP_SERVER_URI: ldaps://ldap.example.com +# DN and password needed to bind to LDAP to retrieve user information +# Can leave blank if anonymous binding is sufficient +# AUTH_LDAP_BIND_DN: "" +# AUTH_LDAP_BIND_PASSWORD: "" +# AUTH_LDAP_USER_SEARCH: "ou=users,dc=example,dc=com" +# AUTH_LDAP_GROUP_SEARCH: "ou=groups,dc=example,dc=com" +# Anyone in this group can get a token to manage images, not superuser +# AUTH_LDAP_STAFF_GROUP_FLAGS: "cn=staff,ou=django,ou=groups,dc=example,dc=com" +# Anyone in this group is a superuser for the app +# AUTH_LDAP_SUPERUSER_GROUP_FLAGS: "cn=superuser,ou=django,ou=groups,dc=example,dc=com" +# OR cn=sregistry_admin,ou=groups,dc=example,dc=com + +# Globus Assocation (globus) +# Only required if 'globus' is added to PLUGINS_ENABLED in config.py +# SOCIAL_AUTH_GLOBUS_KEY: xxxxxxxxxxxx +# SOCIAL_AUTH_GLOBUS_USERNAME: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@clients.auth.globus.org +# SOCIAL_AUTH_GLOBUS_SECRET: xxxxxxxxxxxxxxxx +# GLOBUS_ENDPOINT_ID: myendpoint + +# SAML Authentication (saml) +# Only required if 'saml_auth' is added to PLUGINS_ENABLED +# AUTH_SAML_IDP: stanford +# AUTH_SAML_INSTITUTION: Stanford University diff --git a/shub/dummy_secrets.py b/shub/dummy_secrets.py index fdf00824..7170978e 100644 --- a/shub/dummy_secrets.py +++ b/shub/dummy_secrets.py @@ -1,13 +1,9 @@ -# This file, dummy_secrets, provides an example of how to configure -# sregistry with your authentication secrets. Copy it to secrets.py and -# configure the settings you need. - +# These dummy secrets are provided as an example. +# You can also use the dummy-settings.yaml -> settings.yaml instead. # Secret Key -# You must uncomment, and set SECRET_KEY to a secure random value -# e.g. https://djskgen.herokuapp.com/ - -SECRET_KEY = "xxxxxxxxxxxxxxxxxx" +# e.g. use a generator https://djskgen.herokuapp.com/ +# SECRET_KEY=xxxxxxxxxxxxxxxxxxxx # ============================================================================= # Social Authentication @@ -75,7 +71,7 @@ # ============================================================================= # GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json" -# SREGISTRY_GOOGLE_PROJECT="myproject-ftw" +# GOOGLE_PROJECT="myproject-ftw" # GOOGLE_BUILD_CACHE="true" # After build, do not delete intermediate dependencies in cloudbuild bucket (keep them as cache for rebuild if needed). @@ -88,7 +84,7 @@ # GOOGLE_BUILD_SINGULARITY_VERSION="v3.2.1-slim" # if you want to specify a version of Singularity. The version must coincide with a container tag hosted under singularityware/singularity. The version will default to 3.2.0-slim If you want to use a different version, update this variable. -# SREGISTRY_GOOGLE_STORAGE_BUCKET="taco-singularity-registry" +# GOOGLE_STORAGE_BUCKET="taco-singularity-registry" # is the name for the bucket you want to create. The example here is using the unique identifier appended with “sregistry-" # If you don't define it, it will default to a string that includes the hostname. # Additionally, a temporary bucket is created with the same name ending in _cloudbuild. This bucket is for build time dependencies, and is cleaned up after the fact. If you are having trouble getting a bucket it is likely because the name is taken, diff --git a/shub/plugins/google_build/utils.py b/shub/plugins/google_build/utils.py index 43a8b40c..c174f2b0 100644 --- a/shub/plugins/google_build/utils.py +++ b/shub/plugins/google_build/utils.py @@ -368,8 +368,8 @@ def get_bucket_name(object_name): to get from the client settings, otherwise we get from the object. """ # First default to what is defined with server - if hasattr(settings, "SREGISTRY_GOOGLE_STORAGE_BUCKET"): - return settings.SREGISTRY_GOOGLE_STORAGE_BUCKET + if hasattr(settings, "GOOGLE_STORAGE_BUCKET"): + return settings.GOOGLE_STORAGE_BUCKET # https://www.googleapis.com/download/storage/v1/b//o parts = re.split("/v[0-9]{1}/b/", object_name) diff --git a/shub/settings.py b/shub/settings.py index 725de8cd..3be8e638 100644 --- a/shub/settings.py +++ b/shub/settings.py @@ -11,6 +11,7 @@ from importlib import import_module import yaml import os +import sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -32,11 +33,6 @@ "ENABLE_GITLAB_AUTH": False, "ENABLE_BITBUCKET_AUTH": False, "ENABLE_GITHUB_ENTERPRISE_AUTH": False, - # NOTE you will need to set authentication methods up. - # Configuration goes into secrets.py - # see https://singularityhub.github.io/sregistry/install.html - # secrets.py.example provides a template to work from - # Allow users to create public collections "USER_COLLECTIONS": True, # Should registries by default be private, with no option for public? "PRIVATE_ONLY": False, @@ -58,6 +54,8 @@ "LOGGING_SAVE_RESPONSES": True, # Given that someone goes over, are they blocked for the period? "VIEW_RATE_LIMIT_BLOCK": True, + # If using bitbucket, only allow verified emails + "SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY": None, # True } STRING_DEFAULTS = { @@ -79,15 +77,19 @@ "HELP_INSTITUTION_SITE": "https://srcc.stanford.edu", "REGISTRY_NAME": "Tacosaurus Computing Center", "REGISTRY_URI": "taco", - # "UA-XXXXXXXXX" - "GOOGLE_ANALYTICS": None, + "GOOGLE_ANALYTICS": "", # "UA-XXXXXXXXX" (leave this as empty string so it is set!) "GOOGLE_BUILD_SINGULARITY_VERSION": "v3.3.0-slim", - "SREGISTRY_DATABASE_ENGINE": "django.db.backends.postgresql_psycopg2", - "SREGISTRY_DATABASE_NAME": "postgres", - "SREGISTRY_DATABASE_USER": "postgres", - "SREGISTRY_DATABASE_HOST": "db", - "SREGISTRY_DATABASE_PORT": "5432", + "DATABASE_ENGINE": "django.db.backends.postgresql_psycopg2", + "DATABASE_NAME": "postgres", + "DATABASE_USER": "postgres", + "DATABASE_HOST": "db", + "DATABASE_PORT": "5432", + # Redis + "REDIS_HOST": "redis", + "REDIS_URL": "redis://redis/0", # Minio Storage + "MINIO_ROOT_USER": None, + "MINIO_ROOT_PASSWORD": None, "MINIO_SERVER": "minio:9000", # Internal to sregistry "MINIO_EXTERNAL_SERVER": "127.0.0.1:9000", # minio server for Singularity to interact with "MINIO_BUCKET": "sregistry", @@ -95,8 +97,76 @@ # The rate limit for each view, django-ratelimit, "50 per day per ipaddress) "VIEW_RATE_LIMIT": "50/1d", "DJANGO_LOG_LEVEL": "WARNING", - # An image url or one of the following: 'mm', 'identicon', 'monsterid', 'wavatar', 'retro'. Defaults to 'mm' + # An image url or one of the following: 'mm', 'identicon', 'monsterid', 'wavatar', 'retro'. Defaults to 'mm', and 'retro' here "GRAVATAR_DEFAULT_IMAGE": "retro", + # Twitter Social Authentication + "SOCIAL_AUTH_TWITTER_KEY": None, + "SOCIAL_AUTH_TWITTER_SECRET": None, + # Google social authentication + # http://psa.matiasaguirre.net/docs/backends/google.html?highlight=google + "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": None, # 'xxxxxxxxxxxxxxxxxx.apps.googleusercontent.com' + "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": None, + # Google social authentication + # http://psa.matiasaguirre.net/docs/backends/github.html?highlight=github + "SOCIAL_AUTH_GITHUB_KEY": None, + "SOCIAL_AUTH_GITHUB_SECRET": None, + # GitHub Enterprise + "SOCIAL_AUTH_GITHUB_ENTERPRISE_URL": None, + # Set the API URL for your GitHub Enterprise appliance: + "SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL": None, + # Fill the Client ID and Client Secret values from GitHub in the settings + "SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY": None, + "SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET": None, + # GitLab OAuth2 + "SOCIAL_AUTH_GITLAB_KEY": None, + "SOCIAL_AUTH_GITLAB_SECRET": None, + # Google Cloud Build + Storage: configure a custom builder and storage endpoint + "GOOGLE_APPLICATION_CREDENTIALS": None, # /path/to/credentials.json + "GOOGLE_PROJECT": None, + # After build, do not delete intermediate dependencies in cloudbuild bucket (keep them as cache for rebuild if needed). + # Defaults to being unset, meaning that files are cleaned up. If you define this as anything, the build files will be cached. + "GOOGLE_BUILD_CACHE": None, # "true" + # if you want to specify a version of Singularity. The version must coincide with a container tag hosted under singularityware/singularity. + # The version will default to 3.2.0-slim If you want to use a different version, update this variable. + "GOOGLE_BUILD_SINGULARITY_VERSION": None, # "v3.2.1-slim" + # is the name for the bucket you want to create. The example here is using the unique identifier appended with “sregistry-" + # If you don't define it, it will default to a string that includes the hostname. + # Additionally, a temporary bucket is created with the same name ending in _cloudbuild. This bucket is for build time dependencies, + # and is cleaned up after the fact. If you are having trouble getting a bucket it is likely because the name is taken, + # and we recommend creating both and _cloudbuild in the console and then setting the name here. + "GOOGLE_STORAGE_BUCKET": None, # taco-singularity-registry" + # Bitbucket social auth + "SOCIAL_AUTH_BITBUCKET_OAUTH2_KEY": None, # '' + "SOCIAL_AUTH_BITBUCKET_OAUTH2_SECRET": None, # '' + # LDAP Authentication (ldap-auth) + # Only required if 'ldap-auth' is added to PLUGINS_ENABLED in config.py + # This example assumes you are using an OpenLDAP directory + # If using an alternative directory - e.g. Microsoft AD, 389 you + # will need to modify attribute names/mappings accordingly + # See https://django-auth-ldap.readthedocs.io/en/1.2.x/index.html + # The URI to our LDAP server (may be ldap:// or ldaps://) + "AUTH_LDAP_SERVER_URI": None, # "ldaps://ldap.example.com + # DN and password needed to bind to LDAP to retrieve user information + # Can leave blank if anonymous binding is sufficient + "AUTH_LDAP_BIND_DN": "", + "AUTH_LDAP_BIND_PASSWORD": "", + "AUTH_LDAP_USER_SEARCH": None, # "ou=users,dc=example,dc=com" + "AUTH_LDAP_GROUP_SEARCH": None, # "ou=groups,dc=example,dc=com" + # Anyone in this group can get a token to manage images, not superuser + "AUTH_LDAP_STAFF_GROUP_FLAGS": None, # "cn=staff,ou=django,ou=groups,dc=example,dc=com", + # Anyone in this group is a superuser for the app + "AUTH_LDAP_SUPERUSER_GROUP_FLAGS": None, # "cn=superuser,ou=django,ou=groups,dc=example,dc=com" + # "cn=sregistry_admin,ou=groups,dc=example,dc=com" + # Globus Assocation (globus) + # Only required if 'globus' is added to PLUGINS_ENABLED in config.py + "SOCIAL_AUTH_GLOBUS_KEY": None, + "SOCIAL_AUTH_GLOBUS_USERNAME": None, # "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@clients.auth.globus.org" + "SOCIAL_AUTH_GLOBUS_SECRET": None, + "GLOBUS_ENDPOINT_ID": None, + # SAML Authentication (saml) + # Only required if 'saml_auth' is added to PLUGINS_ENABLED + "AUTH_SAML_IDP": None, # "stanford" + "AUTH_SAML_INSTITUTION": None, # "Stanford University" } INTEGER_DEFAULTS = { @@ -114,8 +184,21 @@ "GOOGLE_BUILD_LIMIT": 100, "GOOGLE_BUILD_TIMEOUT_SECONDS": None, # None defaults to 10 minutes "GOOGLE_BUILD_EXPIRE_SECONDS": 28800, - "CONTAINER_SIGNED_URL_EXPIRE_SECONDS": 10, "MINIO_SIGNED_URL_EXPIRE_MINUTES": 5, + # Google Build + # To prevent denial of service attacks on Google Cloud Storage, you should set a reasonable limit for the number of active, concurrent builds. + # This number should be based on your expected number of users, repositories, and recipes per repository. + "GOOGLE_BUILD_LIMIT": None, # 100 + # The number of seconds for the build to timeout. If set to None, will be 10 minutes. If + # unset, will default to 3 hours. This time should be less than the GOOGLE_BUILD_EXPIRE_SECONDS + "GOOGLE_BUILD_TIMEOUT_SECONDS": None, + # The number of seconds for the build to expire, meaning it's response is no longer accepted by the server. This must be defined. + # Using 28800 would indicate 8 hours (in seconds) + "GOOGLE_BUILD_EXPIRE_SECONDS": None, # 28800 + # The number of seconds to expire a signed URL given to download a container + # from storage. This can be much smaller than 10, as we only need it to endure + # for the POST. + "CONTAINER_SIGNED_URL_EXPIRE_SECONDS": None, # 10 } # Environment helpers @@ -127,50 +210,46 @@ def get_sregistry_envar(key): def get_sregistry_envar_list(key): + key = "SREGISTRY_%s" % key envar_list = os.environ.get(key) or [] if envar_list: envar_list = [x.strip() for x in envar_list.split(",") if x.strip()] return envar_list -# Try reading in from secrets first +# Try reading in from secrets first (no issue if not found) try: from .secrets import * - -# Fallback to dummy secrets (usually for testing) except ImportError: - from .dummy_secrets import * - pass - # Set boolean defaults from environment for key in BOOLEAN_DEFAULTS: value = get_sregistry_envar(key) - if not value: + if value is None: continue # Setting a boolean - if value.lower() == "true": + if value.lower() in ["true", "1"]: BOOLEAN_DEFAULTS[key] = True - elif value.lower() == "false": + elif value.lower() in ["false", "1"]: BOOLEAN_DEFAULTS[key] = False # Set Integer values from the environment for key in INTEGER_DEFAULTS: value = get_sregistry_envar(key) - if value: + if value is not None: try: INTEGER_DEFAULTS[key] = int(value) - except: - print("There was an issue setting %s as an integer." % key) + except ValueError: + sys.exit("There was an issue setting %s as an integer." % key) # Set string environment variables for key in STRING_DEFAULTS: value = get_sregistry_envar(key) - if value: + if value is not None: STRING_DEFAULTS[key] = value # Finally, create settings object @@ -196,7 +275,11 @@ def __iter__(self): if os.path.exists(SETTINGS_FILE): with open(SETTINGS_FILE, "r") as fd: custom_settings = yaml.load(fd.read(), Loader=yaml.FullLoader) - DEFAULTS = DEFAULTS.update(custom_settings) + # Only update if it's set + for k, v in custom_settings.items(): + if v is None: + continue + DEFAULTS[k] = v cfg = Settings(DEFAULTS) @@ -297,7 +380,7 @@ def __iter__(self): ) -# If these need to be exposed via envar, we can do. +# If these need to be exposed via envar, we can do (they aren't currently used) # http://psa.matiasaguirre.net/docs/configuration/settings.html#urls-options # SOCIAL_AUTH_REDIRECT_IS_HTTPS = True # SOCIAL_AUTH_USER_MODEL = 'django.contrib.auth.models.User' @@ -317,7 +400,7 @@ def __iter__(self): DOMAIN_NAKED = cfg.DOMAIN_NAME_HTTP.replace("http://", "") -envar_admins = get_sregistry_envar_list("SREGISTRY_ADMINS") +envar_admins = get_sregistry_envar_list("ADMINS") ADMINS = (("vsochat", "@vsoch"),) if envar_admins: ADMINS = tuple(envar_admins) @@ -330,25 +413,24 @@ def __iter__(self): DATABASES = { "default": { - "ENGINE": cfg.SREGISTRY_DATABASE_ENGINE, - "NAME": cfg.SREGISTRY_DATABASE_NAME, - "USER": cfg.SREGISTRY_DATABASE_USER, - "HOST": cfg.SREGISTRY_DATABASE_HOST, - "PORT": cfg.SREGISTRY_DATABASE_PORT, + "ENGINE": cfg.DATABASE_ENGINE, + "NAME": cfg.DATABASE_NAME, + "USER": cfg.DATABASE_USER, + "HOST": cfg.DATABASE_HOST, + "PORT": cfg.DATABASE_PORT, } } # STORAGE -MINIO_ROOT_USER = os.environ.get("MINIO_ROOT_USER") -MINIO_ROOT_PASSWORD = os.environ.get("MINIO_ROOT_PASSWORD") +MINIO_ROOT_USER = os.environ.get("MINIO_ROOT_USER") or cfg.MINIO_ROOT_USER +MINIO_ROOT_PASSWORD = os.environ.get("MINIO_ROOT_PASSWORD") or cfg.MINIO_ROOT_PASSWORD if not MINIO_ROOT_USER or not MINIO_ROOT_PASSWORD: print( "Warning: either MINIO_ROOT_USER or MINIO_ROOT_PASSWORD is missing, storage may not work." ) - # Plugins # Add the name of a plugin under shub.plugins here to enable it @@ -371,8 +453,10 @@ def __iter__(self): ] # Any plugins enabled from the environment? -PLUGINS_ENABLED += get_sregistry_envar_list("SREGISTRY_PLUGINS_ENABLED") +PLUGINS_ENABLED += get_sregistry_envar_list("PLUGINS_ENABLED") +# Ensure unique set +PLUGINS_ENABLED = list(set(PLUGINS_ENABLED)) # Default Django logging is WARNINGS+ to console # so visible via docker-compose logs uwsgi @@ -473,12 +557,9 @@ def __iter__(self): UPLOAD_PATH = "%s/_upload" % MEDIA_ROOT -RQ_QUEUES = {"default": {"URL": os.getenv("REDIS_URL", "redis://redis/0")}} - -RQ = {"host": "redis", "db": 0} +RQ_QUEUES = {"default": {"URL": cfg.REDIS_URL}} +RQ = {"host": cfg.REDIS_HOST, "db": 0} -# background tasks -BACKGROUND_TASK_RUN_ASYNC = True # Plugins @@ -486,6 +567,43 @@ def __iter__(self): if "pam_auth" in PLUGINS_ENABLED: INSTALLED_APPS += ["django_pam"] +# If LDAP_AUTH in plugins enabled, populate from settings +if "ldap_auth" in PLUGINS_ENABLED: + + # To work with OpenLDAP and posixGroup groups we need to import some things + import ldap + from django_auth_ldap.config import LDAPSearch, PosixGroupType + + # DN and password needed to bind to LDAP to retrieve user information + # Can leave blank if anonymous binding is sufficient + AUTH_LDAP_BIND_DN = cfg.AUTH_LDAP_BIND_DN or "" + AUTH_LDAP_BIND_PASSWORD = cfg.AUTH_LDAP_BIND_PASSWORD or "" + + # Any user account that has valid auth credentials can login + AUTH_LDAP_USER_SEARCH = LDAPSearch( + cfg.AUTH_LDAP_USER_SEARCH, ldap.SCOPE_SUBTREE, "(uid=%(user)s)" + ) + AUTH_LDAP_GROUP_SEARCH = LDAPSearch( + cfg.AUTH_LDAP_GROUP_SEARCH, ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" + ) + AUTH_LDAP_GROUP_TYPE = PosixGroupType() + + # Populate the Django user model from the LDAP directory. + AUTH_LDAP_USER_ATTR_MAP = { + "first_name": "givenName", + "last_name": "sn", + "email": "mail", + } + + # Map LDAP group membership into Django admin flags + AUTH_LDAP_USER_FLAGS_BY_GROUP = { + # Anyone in this group can get a token to manage images, not superuser + "is_staff": cfg.AUTH_LDAP_STAFF_GROUP_FLAGS, + # Anyone in this group is a superuser for the app + "is_superuser": cfg.AUTH_LDAP_SUPERUSER_GROUP_FLAGS, + } + + # If google_build in use, we are required to include GitHub if "google_build" in PLUGINS_ENABLED: @@ -522,16 +640,23 @@ def __iter__(self): # Finally, ensure all variables in cfg are set in locals for key, value in cfg: + + # Don't set if the value is empty, or it's been set previously + if value is None or key in locals() and locals()[key] is not None: + continue locals()[key] = value -# Add SREGISTRY_SECRET_ from the environment -for key, value in os.environ.items(): - if key.startswith("SREGISTRY_SECRET_"): - secret_key = key.replace("SREGISTRY_SECRET_", "") - if not value: - continue - if value.lower() == "true": - value = True - elif value.lower() == "false": - value = False - locals()[secret_key] = value +# If we don't have a secret key, no go +if "SECRET_KEY" not in locals(): + sys.exit("SECRET_KEY is required but not set. Set SREGISTRY_SECRET_KEY.") + + +if ENABLE_GOOGLE_AUTH: + # The scope is not needed, unless you want to develop something new. + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = { + "access_type": "offline", + "approval_prompt": "auto", + } + +if ENABLE_GITLAB_AUTH: + SOCIAL_AUTH_GITLAB_SCOPE = ["api", "read_user"]