Skip to content

Commit

Permalink
Merge pull request #43 from QuanMPhm/42/oidc_auth
Browse files Browse the repository at this point in the history
Fixed OIDC authentication for SCIM endpoints
  • Loading branch information
knikolla authored Oct 2, 2024
2 parents 5f6341f + 5bf8fc8 commit 45a880b
Show file tree
Hide file tree
Showing 14 changed files with 194 additions and 4 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/test-py39-functional-scim-auth.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: test-py39-functional-scim-auth

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test-scim-auth:
runs-on: ubuntu-latest

steps:
- name: Check out code
uses: actions/checkout@v3

- name: Set up python
uses: actions/setup-python@v4
with:
python-version: 3.9

- name: Run keycloak container
run: |
./ci/run_keycloak.sh
- name: Install Coldfront and plugin
run: |
./ci/setup.sh
- name: Run SCIM authentication functional test
run: |
./ci/run_functional_test_scim_auth.sh
20 changes: 20 additions & 0 deletions ci/run_functional_test_scim_auth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# For onbarding-tools
export KEYCLOAK_USERNAME=admin
export KEYCLOAK_PASSWORD=nomoresecret
export KEYCLOAK_URL="http://localhost:8080"
export OIDC_CLIENT_ID=coldfront
export OIDC_CLIENT_SECRET=nomoresecret
export HORIZON_URL="http://foo"

# For coldfront oidc plugin
export DJANGO_SETTINGS_MODULE="local_settings"
export PLUGIN_AUTH_OIDC=True
export OIDC_RP_CLIENT_ID="coldfront"
export OIDC_RP_CLIENT_SECRET='nomoresecret'
export OIDC_OP_AUTHORIZATION_ENDPOINT="http://localhost:8080/realms/master/protocol/openid-connect/auth"
export OIDC_OP_TOKEN_ENDPOINT="http://localhost:8080/realms/master/protocol/openid-connect/token"
export OIDC_OP_USER_ENDPOINT="http://localhost:8080/realms/master/protocol/openid-connect/userinfo"
export OIDC_RP_SIGN_ALGO='RS256'
export OIDC_OP_JWKS_ENDPOINT="http://localhost:8080/realms/master/protocol/openid-connect/certs"

coldfront test coldfront_plugin_api.tests.functional.test_scim_auth
8 changes: 8 additions & 0 deletions ci/run_keycloak.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
set -xe

sudo docker run -d --name keycloak \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=nomoresecret \
-p 8080:8080 \
-p 8443:8443 \
quay.io/keycloak/keycloak:latest start-dev
1 change: 1 addition & 0 deletions src/coldfront_plugin_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
"GROUP_ADAPTER": "coldfront_plugin_api.scim_v2.adapter_group.SCIMColdfrontGroup",
"GROUP_FILTER_PARSER": "coldfront_plugin_api.scim_v2.filters.ColdfrontGroupFilterQuery",
"GET_IS_AUTHENTICATED_PREDICATE": "coldfront_plugin_api.utils.is_user_superuser",
"AUTH_CHECK_MIDDLEWARE": "coldfront_plugin_api.scim_v2.auth_middleware.SCIMColdfrontAuthCheckMiddleware",
}
20 changes: 20 additions & 0 deletions src/coldfront_plugin_api/scim_v2/auth_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os
import logging

from django_scim.middleware import SCIMAuthCheckMiddleware
from mozilla_django_oidc.contrib.drf import OIDCAuthentication

logger = logging.getLogger(__name__)


class SCIMColdfrontAuthCheckMiddleware(SCIMAuthCheckMiddleware):
def process_request(self, request):
if not request.user or not request.user.is_authenticated:
# django-scim2 does not use by default the DRF backend of mozilla-django-oidc,
# and therefore does not support authentication with bearer tokens, only
# session cookies. We manually call `authenticate()` on the DRF backend if
# the user is not already authenticated, and if OIDC authentication is enabled.
if os.getenv("PLUGIN_AUTH_OIDC") == "True":
if user_tuple := OIDCAuthentication().authenticate(request):
request.user = user_tuple[0]
return super().process_request(request)
File renamed without changes.
Empty file.
65 changes: 65 additions & 0 deletions src/coldfront_plugin_api/tests/functional/test_scim_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import os

from django.test import Client


from coldfront_plugin_api.tests.functional import utils
from coldfront_plugin_api.tests.base import TestBase


class TestAuthOIDC(TestBase):
@classmethod
def setUpClass(self):
super().setUpClass()
self.kc_client = utils.UpdatedKeycloakClient()
# Only initialize Coldfront client if running in Github action
if os.getenv("CI") == "true":
self.kc_client.create_client(
"master",
"coldfront",
"nomoresecret",
["http://foo/signup/oidc_redirect_uri"],
)
self.kc_client.create_user("master", "[email protected]", "staff", "staff")
self.kc_client.create_user("master", "[email protected]", "user", "user")

def setUp(self):
super().setUp()
self.staff_user = self.new_user("[email protected]")
self.normal_user = self.new_user("[email protected]")

self.staff_user.is_staff = True
self.staff_user.save()

def test_oidc_authenticated(self):
# Test for both staff and normal authenticated users
def impersonate_and_get_endpoint(
user_to_impersonate, endpoint_url, expected_status_code
):
user_token = self.kc_client.impersonate_access_token(user_to_impersonate)

cf_client = Client()
r = cf_client.get(
endpoint_url,
HTTP_AUTHORIZATION=f"Bearer {user_token}",
)
self.assertEqual(r.status_code, expected_status_code)

for endpoint_url in [
"/api/scim/v2/Users",
"/api/scim/v2/Groups",
"/api/allocations",
]:
impersonate_and_get_endpoint(self.staff_user.username, endpoint_url, 200)
impersonate_and_get_endpoint(self.normal_user.username, endpoint_url, 403)

def test_oidc_unauthenticated(self):
# Test for unauthenticated user case
cf_client = Client()
for endpoint_url in [
"/api/scim/v2/Users",
"/api/scim/v2/Groups",
"/api/allocations",
]:
r = cf_client.get(endpoint_url)
self.assertEqual(r.status_code, 401)
41 changes: 41 additions & 0 deletions src/coldfront_plugin_api/tests/functional/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import urllib
import requests

from onboarding_tools.keycloak import KeycloakClient
from onboarding_tools import settings


class UpdatedKeycloakClient(KeycloakClient):
@staticmethod
def construct_url(realm, path):
return f"{settings.KEYCLOAK_URL}/admin/realms/{realm}/{path}"

@property
def url_base(self):
return f"{settings.KEYCLOAK_URL}/admin/realms"

@staticmethod
def auth_endpoint(realm):
return f"{settings.KEYCLOAK_URL}/realms/{realm}/protocol/openid-connect/auth"

@staticmethod
def token_endpoint(realm):
return f"{settings.KEYCLOAK_URL}/realms/{realm}/protocol/openid-connect/token"

def impersonate_access_token(self, user):
user_session = requests.session()
user_session.cookies.update(self.impersonate(user).cookies)
params = {
"response_mode": "fragment",
"response_type": "token",
"client_id": settings.OIDC_CLIENT_ID,
"client_secret": settings.OIDC_CLIENT_SECRET,
"redirect_uri": f"{settings.HORIZON_URL}/signup/oidc_redirect_uri",
"scope": "openid profile email",
}
response = user_session.get(
self.auth_endpoint("master"), params=params, allow_redirects=False
)
redirect = response.headers["Location"]
token = urllib.parse.parse_qs(redirect)["access_token"][0]
return token
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
AllocationUserStatusChoice,
)
from coldfront.core.project.models import ProjectUser
from coldfront_plugin_api.tests.unit import base
from coldfront_plugin_api.tests import base

from unittest.mock import patch

Expand Down
3 changes: 2 additions & 1 deletion src/coldfront_plugin_api/tests/unit/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import uuid

from coldfront.core.resource import models as resource_models
from coldfront_plugin_api.tests import base
from rest_framework.test import APIClient

from coldfront_plugin_api.tests.unit import base, fakes
from coldfront_plugin_api.tests.unit import fakes


def get_payload_for_single_operation(operation, username):
Expand Down
3 changes: 2 additions & 1 deletion src/coldfront_plugin_api/tests/unit/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

from coldfront.core.resource import models as resource_models
from coldfront.core.user.models import User
from coldfront_plugin_api.tests import base
from rest_framework.test import APIClient

from coldfront_plugin_api.tests.unit import base, fakes
from coldfront_plugin_api.tests.unit import fakes


class TestUsers(base.TestBase):
Expand Down
2 changes: 1 addition & 1 deletion src/coldfront_plugin_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def is_user_superuser(user: User):
As a temporary hack, this function will handle raising the appropriate 403 error if
user is authenticated, but not superuser
"""
if user.is_authenticated and not user.is_superuser:
if user.is_authenticated and not (user.is_staff or user.is_superuser):
raise PermissionDenied
else:
return user.is_authenticated
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
git+https://github.com/nerc-project/coldfront-plugin-cloud@main#egg=coldfront_plugin_cloud
git+https://github.com/CCI-MOC/onboarding-tools@master#egg=onboarding_tools

0 comments on commit 45a880b

Please sign in to comment.