Skip to content

Auth OIDC #6

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

Open
wants to merge 78 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
aab4750
extended configuration manager with optional OIDC sections
sjanssen2 Mar 20, 2024
49b0448
flake8
sjanssen2 Mar 20, 2024
2840601
also provide a label for a speaking name of the identity provider
sjanssen2 Mar 20, 2024
f1c9149
start implementing the OIDC dance
sjanssen2 Mar 20, 2024
2eb6d08
modal not necessary, if only one provider was defined
sjanssen2 Mar 20, 2024
48ca02a
error handling of provider not in config file
sjanssen2 Mar 20, 2024
dc4bd20
adding pycurl package to enable tornado curl_httpclients
sjanssen2 Mar 20, 2024
e1f3c13
a new method to create a user, if information do not need to be enter…
sjanssen2 Mar 20, 2024
48f09a5
full OIDC dance implemented
sjanssen2 Mar 20, 2024
baf40df
add an admin page to activate users which requested authorization thr…
sjanssen2 Mar 20, 2024
670a55a
flake8
sjanssen2 Mar 20, 2024
091ffc6
adding menu entry for user authorization
sjanssen2 Mar 20, 2024
1feefc0
do not expose traditional qiita internal user authentication, if OIDC…
sjanssen2 Mar 21, 2024
29ce7dd
use Qiita typical modal for OIDC login
sjanssen2 Mar 21, 2024
2ca5bb8
wrong menu entrie affected
sjanssen2 Mar 21, 2024
1b787cb
always allow logout
sjanssen2 Mar 21, 2024
88319b2
improved error handling
sjanssen2 Mar 21, 2024
02d9af0
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Mar 22, 2024
b1e1b6b
revert: let user change their profile, but not password - if provided…
sjanssen2 Mar 22, 2024
a7d3b84
speaking button names + move into correct div to always get displayed
sjanssen2 Mar 22, 2024
125835a
use email from config + loop user_info from OIDC to fill DB
sjanssen2 Mar 22, 2024
5f28092
use OIDC info to prefil user information
sjanssen2 Mar 22, 2024
19b4d7b
drop admin user authorization
sjanssen2 Apr 4, 2024
33f2879
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Apr 4, 2024
c9d413a
using the well-known json dict instead of manually providing multiple…
sjanssen2 Jun 5, 2024
6bfafcb
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 5, 2024
9a5e7cc
flake8
sjanssen2 Jun 5, 2024
b2fc279
flake8
sjanssen2 Jun 5, 2024
5cc0896
add ability to display OIDC logos
sjanssen2 Jun 5, 2024
949084d
add OIDC logo
sjanssen2 Jun 5, 2024
c3b040b
revert to dev branch
sjanssen2 Jun 5, 2024
d96bbae
fixing config manager tests
sjanssen2 Jun 5, 2024
a491870
Merge pull request #7 from jlab/auth_oidc_wellknown
sjanssen2 Jun 5, 2024
b1baece
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 6, 2024
81fdcbf
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 20, 2024
e0c4002
add missing template
sjanssen2 Jun 20, 2024
bb9c685
Merge branch 'add_admin_purge_template' of github.com:jlab/qiita into…
sjanssen2 Jun 20, 2024
79e794a
Merge branch 'dev' of https://github.com/qiita-spots/qiita into auth_…
sjanssen2 Jun 21, 2024
c0e715b
Update CHANGELOG.md
antgonza Oct 12, 2024
d8cb8db
Merge branch 'dev' of github.com:biocore/qiita
antgonza Jan 13, 2025
fdad618
Merge branch 'dev' of github.com:biocore/qiita
antgonza Feb 25, 2025
c9aacec
Merge branch 'master' of github.com:qiita-spots/qiita into auth_oidc_…
sjanssen2 Mar 4, 2025
a5deb83
Merge pull request #10 from jlab/auth_oidc_merged
sjanssen2 Mar 4, 2025
7693c5e
extended configuration manager with optional OIDC sections
sjanssen2 Mar 20, 2024
b4ab605
flake8
sjanssen2 Mar 4, 2025
baa7230
also provide a label for a speaking name of the identity provider
sjanssen2 Mar 20, 2024
52e57ca
start implementing the OIDC dance
sjanssen2 Mar 20, 2024
4061373
modal not necessary, if only one provider was defined
sjanssen2 Mar 20, 2024
51307d1
error handling of provider not in config file
sjanssen2 Mar 20, 2024
7a0ec9f
adding pycurl package to enable tornado curl_httpclients
sjanssen2 Mar 20, 2024
0c365a1
a new method to create a user, if information do not need to be enter…
sjanssen2 Mar 20, 2024
e993a99
full OIDC dance implemented
sjanssen2 Mar 20, 2024
ca5f7f6
add an admin page to activate users which requested authorization thr…
sjanssen2 Mar 20, 2024
4d5c6a2
flake8
sjanssen2 Mar 20, 2024
fd6d15e
adding menu entry for user authorization
sjanssen2 Mar 20, 2024
9c8b824
do not expose traditional qiita internal user authentication, if OIDC…
sjanssen2 Mar 21, 2024
a654e48
use Qiita typical modal for OIDC login
sjanssen2 Mar 21, 2024
27f6d35
always allow logout
sjanssen2 Mar 21, 2024
85bf1fa
improved error handling
sjanssen2 Mar 21, 2024
8a504cc
revert: let user change their profile, but not password - if provided…
sjanssen2 Mar 22, 2024
ef05eed
speaking button names + move into correct div to always get displayed
sjanssen2 Mar 22, 2024
a5270a0
use email from config + loop user_info from OIDC to fill DB
sjanssen2 Mar 22, 2024
2efb70f
use OIDC info to prefil user information
sjanssen2 Mar 22, 2024
c8b1198
drop admin user authorization
sjanssen2 Apr 4, 2024
3d6f718
using the well-known json dict instead of manually providing multiple…
sjanssen2 Jun 5, 2024
3957030
flake8
sjanssen2 Jun 5, 2024
648f2f9
flake8
sjanssen2 Jun 5, 2024
73f92b9
add ability to display OIDC logos
sjanssen2 Jun 5, 2024
0dc243d
add OIDC logo
sjanssen2 Jun 5, 2024
9b81163
fixing config manager tests
sjanssen2 Jun 5, 2024
bb03167
Merge branch 'auth_oidc' of github.com:jlab/qiita into auth_oidc
sjanssen2 Mar 4, 2025
0a29ac2
create neccessary "mountpoints" (#3462)
sjanssen2 Mar 9, 2025
a76288b
Merge branch 'master' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 11, 2025
8cc718f
Merge branch 'dev' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 11, 2025
5ab3ebb
fix sub-directory path (#3464)
sjanssen2 Mar 11, 2025
89cab41
Merge branch 'master' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 11, 2025
d0e03de
Fix multiple validation jobs (#3465)
sjanssen2 Mar 12, 2025
1a61930
Merge branch 'dev' of github.com:qiita-spots/qiita into auth_oidc
sjanssen2 Mar 12, 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
2 changes: 1 addition & 1 deletion .github/workflows/qiita-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
# Setting up main qiita conda environment
conda config --add channels conda-forge
conda deactivate
conda create --quiet --yes -n qiita python=3.9 pip libgfortran numpy nginx cython redis
conda create --quiet --yes -n qiita python=3.9 pip libgfortran numpy nginx cython redis pycurl
conda env list
conda activate qiita
pip install -U pip
Expand Down
67 changes: 67 additions & 0 deletions qiita_core/configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,32 @@ class ConfigurationManager(object):
The email address a user should write to when asking for help
sysadmin_email : str
The email address, Qiita sends internal notifications to a sys admin
None (=internal user authentication) or one or several 'oidc_' sections
to use external identity providers (IdP) with following values:
client_id : str
The name you registered Qiita with at the external IdP
client_secret : str
A secret string with which Qiita identifies at the external IdP (not
all IdPs need a secret)
redirect_endpoint : str
The internal Qiita endpoint the IdP shall redirect the user after
logging in
wellknown_uri : str
The URL of the well-known json document, specifying how API end points
like 'authorize', 'token' or 'userinfo' are defined. See e.g.
https://swagger.io/docs/specification/authentication/
openid-connect-discovery/
label : str
A speaking label for the Identity Provider
scope : str
The scope, i.e. fields about a user, which Qiita requests from the
Identity Provider, e.g. "profile email eduperson_orcid".
Will be automatically extended by the scope "openid", to enable the
"authorize_code" OIDC flow.
logo : str
Optional. Name of a file in qiita_pet/static/img that shall be
displayed for login through Service Provider, instead of a plain
button

Raises
------
Expand Down Expand Up @@ -162,6 +188,7 @@ def __init__(self):
self._get_vamps(config)
self._get_portal(config)
self._iframe(config)
self._get_oidc(config)

def _get_main(self, config):
"""Get the configuration of the main section"""
Expand Down Expand Up @@ -390,3 +417,43 @@ def _get_portal(self, config):

def _iframe(self, config):
self.iframe_qiimp = config.get('iframe', 'QIIMP', fallback=None)

def _get_oidc(self, config):
"""Get the configuration of the open ID connect section(s)
User can provide multiple sections with naming schema oidc_foo where
foo is the name of an Identity Provider - Qiita can handle multiple
Identity Providers simultaneously.
"""
PREFIX = 'oidc_'
self.oidc = dict()
for section_name in config.sections():
if section_name.startswith(PREFIX):
provider = dict()
provider['client_id'] = config.get(
section_name, 'CLIENT_ID', fallback=None)
provider['client_secret'] = config.get(
section_name, 'CLIENT_SECRET', fallback=None)
provider['redirect_endpoint'] = config.get(
section_name, 'REDIRECT_ENDPOINT')
if provider['redirect_endpoint']:
if not provider['redirect_endpoint'].startswith('/'):
provider['redirect_endpoint'] = '/%s' % provider[
'redirect_endpoint']
if provider['redirect_endpoint'].endswith('/'):
provider['redirect_endpoint'] = provider[
'redirect_endpoint'][:-1]
provider['wellknown_uri'] = config.get(
section_name, 'WELLKNOWN_URI')
provider['label'] = config.get(section_name, 'LABEL')
if not provider['label']:
# fallback, if no label is provided
provider['label'] = section_name[len(PREFIX):]
self.oidc[section_name[len(PREFIX):]] = provider
provider['scope'] = config.get(
section_name, 'SCOPE', fallback=None)
if not provider['scope']:
provider['scope'] = 'openid'
if 'openid' not in provider['scope']:
provider['scope'] = 'openid %s' % provider['scope']
provider['logo'] = config.get(
section_name, 'LOGO', fallback=None)
65 changes: 65 additions & 0 deletions qiita_core/support_files/config_test.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,68 @@ STATS_MAP_CENTER_LONGITUDE =
# On May 2024, we removed QIIMP from the code base but we will leave this
# section in case we need to add access to another iframe in the future; note
# that the qiita-terms are also accessed via iframe but this is internal

# --------------------- External Identity Provider settings --------------------
# user authentication happens per default within Qiita, i.e. when a user logs in,
# the stored password hash and email address is compared against what a user
# just provided. You might however, use an external identity provider (IdP) to
# authenticate the user like
# google: https://developers.google.com/identity/protocols/oauth2 or
# github: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps or
# self hosted keycloak: https://www.keycloak.org/
# Thus, you don't have to deal with user verification, reset passwords, ...
# Authorization (i.e. if the authorized user is allowed to use Qiita or which
# user level he/she gets assigned is an independent process. You can even use
# multiple independent external identity providers!
# Qiita currently only support the "open ID connect" protocol with the implicit flow.
# Each identity provider comes as its own config section [oidc_foo] and needs
# to specify the following five fields:
#
# Typical identity provider manage multiple "realms" and specific "clients" per realm
# You need to contact your IdP and register Qiita as a new "client". The IdP will
# provide you with the correct values.
#
# The authorization protocol requires three steps to obtain user information:
# 1) you identify as the correct client and ask the IdP for a request code
# You have to forward the user to the login page of your IdP. To let the IdP
# know how to come back to Qiita, you need to provide a redirect URL
# 2) you exchange the code for a user token
# 3) you obtain information about the user for the obtaines user token
# Typically, each step is implemented as a separate URL endpoint
#
# To activate IdP: comment out the following config section

#[oidc_academicid]
#
## client ID for Qiita as registered at your Identity Provider of choice
#CLIENT_ID = gi-qiita-prod
#
## client secret to verify Qiita as the correct client. Not all IdPs require
## a client secret!
#CLIENT_SECRET = verySecretString
#
## redirect URL (end point in your Qiita instance), to which the IdP redirects
## after user types in his/her credentials. If you don't want to change code in
## qiita_pet/webserver.py the URL must follow the pattern:
## base_URL/auth/login_OIDC/foo where foo is the name of this config section
## without the oidc_ prefix!
#REDIRECT_ENDPOINT = /auth/login_OIDC/localkeycloak
#
## The URL of the well-known json document, specifying how API end points
## like 'authorize', 'token' or 'userinfo' are defined. See e.g.
## https://swagger.io/docs/specification/authentication/
## openid-connect-discovery/
#WELLKNOWN_URI = https://keycloak.sso.gwdg.de/.well-known/openid-configuration
#
## a speaking label for the Identity Provider. Section name is used if empty.
#LABEL = GWDG Academic Cloud
#
## The scope, i.e. fields about a user, which Qiita requests from the
## Identity Provider, e.g. "profile email eduperson_orcid".
## Will be automatically extended by the scope "openid", to enable the
## "authorize_code" OIDC flow.
#SCOPE = openid
#
##Optional. Name of a file in qiita_pet/static/img that shall be
##displayed for login through Service Provider, instead of a plain button
#LOGO = oidc_lifescienceAAI.png
85 changes: 85 additions & 0 deletions qiita_core/tests/test_configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,55 @@ def test_get_portal_latlong(self):
obs._get_portal(self.conf)
self.assertEqual(obs.stats_map_center_longitude, -105.24827)

def test_get_oidc(self):
SECTION_NAME = 'oidc_academicid'
obs = ConfigurationManager()
self.assertTrue(len(obs.oidc), 1)
self.assertTrue(obs.oidc.keys(), [SECTION_NAME])

# assert endpoint starts with /
self.conf.set(SECTION_NAME, 'REDIRECT_ENDPOINT', 'auth/something')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['redirect_endpoint'],
'/auth/something')

# assert endpoint does not end with /
self.conf.set(SECTION_NAME, 'REDIRECT_ENDPOINT', 'auth/something/')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['redirect_endpoint'],
'/auth/something')

self.conf.set(SECTION_NAME, 'CLIENT_ID', 'foo')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['client_id'], "foo")

self.assertTrue('gwdg.de' in obs.oidc['academicid']['wellknown_uri'])

self.assertEqual(obs.oidc['academicid']['label'],
'GWDG Academic Cloud')
# test fallback, if no label is provided
self.conf.set(SECTION_NAME, 'LABEL', '')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['label'], 'academicid')

self.assertEqual(obs.oidc['academicid']['scope'], 'openid')
# test fallback, if no scope is provided
self.conf.set(SECTION_NAME, 'SCOPE', '')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['scope'], 'openid')

# test if scope will be automatically extended with 'openid'
self.conf.set(SECTION_NAME, 'SCOPE', 'email affiliation')
obs._get_oidc(self.conf)
self.assertTrue('openid' in obs.oidc['academicid']['scope'].split())

self.assertEqual(obs.oidc['academicid']['logo'],
'oidc_lifescienceAAI.png')
# test fallback, if no scope is provided
self.conf.remove_option(SECTION_NAME, 'LOGO')
obs._get_oidc(self.conf)
self.assertEqual(obs.oidc['academicid']['logo'], None)


CONF = """
# ------------------------------ Main settings --------------------------------
Expand Down Expand Up @@ -471,6 +520,42 @@ def test_get_portal_latlong(self):

# ----------------------------- iframes settings ---------------------------
[iframe]

# ------------------- External Identity Provider settings ------------------
[oidc_academicid]

# client ID for Qiita as registered at your Identity Provider of choice
CLIENT_ID = gi-qiita-prod

# client secret to verify Qiita as the correct client. Not all IdPs require
# a client secret.
CLIENT_SECRET = verySecretString

# redirect URL (end point in your Qiita instance), to which the IdP redirects
# after user types in his/her credentials. If you don't want to change code in
# qiita_pet/webserver.py the URL must follow the pattern:
# base_URL/auth/login_OIDC/foo where foo is the name of this config section
# without the oidc_ prefix!
REDIRECT_ENDPOINT = /auth/login_OIDC/academicid

# The URL of the well-known json document, specifying how API end points
# like 'authorize', 'token' or 'userinfo' are defined. See e.g.
# https://swagger.io/docs/specification/authentication/
# openid-connect-discovery/
WELLKNOWN_URI = https://keycloak.sso.gwdg.de/.well-known/openid-configuration

# a speaking label for the Identity Provider. Section name is used if empty.
LABEL = GWDG Academic Cloud

# The scope, i.e. fields about a user, which Qiita requests from the
# Identity Provider, e.g. "profile email eduperson_orcid".
# Will be automatically extended by the scope "openid", to enable the
# "authorize_code" OIDC flow.
SCOPE = openid

# Optional. Name of a file in qiita_pet/static/img that shall be
# displayed for login through Service Provider, instead of a plain button
LOGO = oidc_lifescienceAAI.png
"""

if __name__ == '__main__':
Expand Down
34 changes: 34 additions & 0 deletions qiita_db/environment_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# -----------------------------------------------------------------------------

from os.path import abspath, dirname, join, exists, basename, splitext
from shutil import copytree
from functools import partial
from os import mkdir
import gzip
Expand Down Expand Up @@ -127,6 +128,36 @@ def _download_reference_files():
_insert_processed_params(ref)


def create_mountpoints():
r"""In a fresh qiita setup, sub-directories under
qiita_config.base_data_dir might not yet exist. To avoid failing in
later steps, they are created here.
"""
with qdb.sql_connection.TRN:
sql = """SELECT DISTINCT mountpoint FROM qiita.data_directory
WHERE active = TRUE"""
qdb.sql_connection.TRN.add(sql)
created_subdirs = []
for mountpoint in qdb.sql_connection.TRN.execute_fetchflatten():
for (ddid, subdir) in qdb.util.get_mountpoint(mountpoint,
retrieve_all=True):
if not exists(join(qiita_config.base_data_dir, subdir)):
if qiita_config.test_environment:
# if in test mode, we want to potentially fill the
# new directory with according test data
copytree(get_support_file('test_data', mountpoint),
join(qiita_config.base_data_dir, subdir))
else:
# in production mode, an empty directory is created
mkdir(join(qiita_config.base_data_dir, subdir))
created_subdirs.append(subdir)

if len(created_subdirs) > 0:
print("Created %i sub-directories as 'mount points':\n%s"
% (len(created_subdirs),
''.join(map(lambda x: ' - %s\n' % x, created_subdirs))))


def make_environment(load_ontologies, download_reference, add_demo_user):
r"""Creates the new environment specified in the configuration

Expand Down Expand Up @@ -397,6 +428,9 @@ def patch(patches_dir=PATCHES_DIR, verbose=False, test=False):
with qdb.sql_connection.TRN:
_populate_test_db()

# create mountpoints as subdirectories in BASE_DATA_DIR
create_mountpoints()

patch_update_sql = "UPDATE settings SET current_patch = %s"
for sql_patch_fp in sql_patch_files[next_patch_index:]:
sql_patch_filename = basename(sql_patch_fp)
Expand Down
18 changes: 14 additions & 4 deletions qiita_db/processing_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,13 +1105,23 @@ def submit(self, parent_job_id=None, dependent_jobs_list=None):
# organized into n 'queues' or 'chains', and
# will all run simultaneously.
for dependent in dependent_jobs_list:
# register dependent job as queued to make qiita
# aware of this child process
dependent._set_status('queued')

dep_software = dependent.command.software
dep_job_dir = join(qdb.util.get_work_base_dir(),
dependent.id)
p = Process(target=launcher['function'],
args=(plugin_env_script,
plugin_start_script,
args=(dep_software.environment_script,
dep_software.start_script,
url,
self.id,
job_dir))
dependent.id,
dep_job_dir))
p.start()
# assign the child process ID as external id to
# the dependent
dependent.external_id = p.pid
else:
error = ("execute_in_process must be defined",
"as either true or false")
Expand Down
1 change: 1 addition & 0 deletions qiita_db/support_files/test_data/FASTQ/blank.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading