Skip to content

Commit

Permalink
Merge 48889d2 into 9263130
Browse files Browse the repository at this point in the history
  • Loading branch information
nuwang authored Dec 13, 2020
2 parents 9263130 + 48889d2 commit 2b15d28
Show file tree
Hide file tree
Showing 32 changed files with 1,949 additions and 330 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
venv
venv3
docs
*.log
*.db
*.sqlite3
12 changes: 11 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ python: 3.6
os:
- linux
# - osx
services:
- docker
env:
- TOX_ENV=py36
global:
- PYTHONUNBUFFERED=True
- COVERALLS_PARALLEL=true
jobs:
- TOX_ENV=py36
- TOX_ENV=py36-integration
matrix:
fast_finish: true
allow_failures:
Expand All @@ -15,8 +22,11 @@ services:
- rabbitmq
install:
- pip install tox
- pip install tox-docker>=2.0.0a3
- pip install coveralls
script:
- tox -e $TOX_ENV
after_success:
- coveralls
notifications:
webhooks: https://coveralls.io/webhook
18 changes: 18 additions & 0 deletions cloudman/cloudman/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import requests

from django.core.cache import cache


def get_metadata(metadata_endpoint):
op_metadata = cache.get('OIDC_OP_METADATA')
if not op_metadata:
response = requests.get(url=metadata_endpoint, verify=False)
response.raise_for_status()
op_metadata = response.json()
cache.set('OIDC_OP_METADATA', op_metadata)
return op_metadata


def get_from_well_known(metadata_endpoint, attr):
metadata = get_metadata(metadata_endpoint)
return metadata.get(attr)
62 changes: 62 additions & 0 deletions cloudman/cloudman/oidc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from cloudman.auth import get_from_well_known
from django.contrib.auth.models import Group
from django.db import transaction
from mozilla_django_oidc import auth, utils, views


def provider_logout(request):
return get_from_well_known(
utils.import_from_settings('OIDC_OP_METADATA_ENDPOINT'),
'end_session_endpoint')


class CMOIDCAuthenticationBackend(auth.OIDCAuthenticationBackend):

def create_user(self, claims):
user = super(CMOIDCAuthenticationBackend, self).create_user(claims)
return self.update_user(user, claims)

def update_user(self, user, claims):
roles = claims.get('roles')
user.first_name = claims.get('given_name', '')
user.last_name = claims.get('family_name', '')
user.is_staff = 'admin' in roles or 'superuser' in roles
user.is_superuser = 'superuser' in roles
user.save()
self.update_groups(user, claims)

return user

def update_groups(self, user, claims):
"""
Transform roles obtained from keycloak into Django Groups and
add them to the user. Note that any role not passed via keycloak
will be removed from the user.
"""
with transaction.atomic():
user.groups.clear()
for role in claims.get('roles'):
group, _ = Group.objects.get_or_create(name=role)
group.user_set.add(user)

def get_userinfo(self, access_token, id_token, payload):
"""
Get user details from the access_token and id_token and return
them in a dict.
"""
userinfo = super().get_userinfo(access_token, id_token, payload)
accessinfo = self.verify_token(access_token, nonce=payload.get('nonce'))
roles = accessinfo.get('realm_access', {}).get('roles', [])

userinfo['roles'] = roles
return userinfo


class OIDCAuthenticationRequestView(views.OIDCAuthenticationRequestView):

def __init__(self, *args, **kwargs):
super(OIDCAuthenticationRequestView, self).__init__(*args, **kwargs)

self.OIDC_OP_AUTH_ENDPOINT = get_from_well_known(
utils.import_from_settings('OIDC_OP_METADATA_ENDPOINT'),
'authorization_endpoint')
54 changes: 33 additions & 21 deletions cloudman/cloudman/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Django settings for cloudman project.
"""
from cloudlaunchserver.settings import *
from cloudman.auth import get_from_well_known

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Expand All @@ -13,8 +14,7 @@

# Application definition
INSTALLED_APPS += [
'bossoidc',
'djangooidc',
'mozilla_django_oidc',
'clusterman',
'helmsman.apps.HelmsManConfig',
'projman',
Expand All @@ -25,32 +25,44 @@
AUTHENTICATION_BACKENDS = [
'rules.permissions.ObjectPermissionBackend',
'django.contrib.auth.backends.ModelBackend',
'bossoidc.backend.OpenIdConnectBackend'
'cloudman.oidc.CMOIDCAuthenticationBackend'
]

REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] += ('oidc_auth.authentication.BearerTokenAuthentication',)

OIDC_ENABLED = os.environ.get('OIDC_ENABLED', False)
MIDDLEWARE += [
'mozilla_django_oidc.middleware.SessionRefresh'
]

# KeyCloak realm url
auth_uri = os.environ.get("OIDC_AUTH_URI") or "http://localhost:8080/auth/realms/master"
# Client ID configured in the Auth Server
client_id = os.environ.get("OIDC_CLIENT_ID") or "cloudman"
# URL of the client
public_uri = os.environ.get("OIDC_PUBLIC_URI") or "http://localhost:4200/cloudman"
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] += ('mozilla_django_oidc.contrib.drf.OIDCAuthentication',)

OIDC_ALLOW_DYNAMIC_OP = False
OIDC_ENABLED = os.environ.get('OIDC_ENABLED', False)

from bossoidc.settings import *
BOSSOIDC_PRESERVE_EXISTING_USER = True
# OIDC settings. Set only if OIDC_ENABLED
OIDC_RP_CLIENT_ID = "cloudman"
OIDC_RP_CLIENT_SECRET = None
OIDC_OP_AUTHORIZATION_ENDPOINT = "http://localhost:8080/auth/realms/master/.well-known/openid-configuration"
OIDC_OP_TOKEN_ENDPOINT = "dummy"
OIDC_OP_USER_ENDPOINT = "dummy"
OIDC_OP_JWKS_ENDPOINT = "dummy"
OIDC_RP_SIGN_ALGO = "RS256"

if OIDC_ENABLED:
LOGIN_URL = "/openid/openid/KeyCloak"
LOGOUT_URL = "/openid/logout"
LOAD_USER_ROLES = 'projman.rules.assign_oidc_roles'
configure_oidc(auth_uri, client_id, public_uri) # NOTE: scope is optional and can be left out
else:
OIDC_PROVIDERS = {}
# KeyCloak realm url
OIDC_OP_METADATA_ENDPOINT = os.environ.get(
"OIDC_METADATA_URI") or "http://localhost:8080/auth/realms/master/.well-known/openid-configuration"
# Client ID configured in the Auth Server
OIDC_RP_CLIENT_ID = os.environ.get("OIDC_CLIENT_ID") or "cloudman"
OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_CLIENT_SECRET")
OIDC_OP_AUTHORIZATION_ENDPOINT = get_from_well_known(OIDC_OP_METADATA_ENDPOINT, 'authorization_endpoint')
OIDC_OP_TOKEN_ENDPOINT = get_from_well_known(OIDC_OP_METADATA_ENDPOINT, 'token_endpoint')
OIDC_OP_USER_ENDPOINT = get_from_well_known(OIDC_OP_METADATA_ENDPOINT, 'userinfo_endpoint')
OIDC_OP_JWKS_ENDPOINT = get_from_well_known(OIDC_OP_METADATA_ENDPOINT, 'jwks_uri')
OIDC_RP_SIGN_ALGO = os.environ.get("OIDC_SIGN_ALGO") or "RS256"
OIDC_USERNAME_ALGO = lambda claim: claim
OIDC_OP_LOGOUT_URL_METHOD = 'cloudman.oidc.provider_logout'
OIDC_AUTHENTICATE_CLASS = 'cloudman.oidc.OIDCAuthenticationRequestView'

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
LOGIN_REDIRECT_URL = "/"

ROOT_URLCONF = 'cloudman.urls'

Expand Down
39 changes: 39 additions & 0 deletions cloudman/cloudman/tests/fixtures/keycloak-export-realm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# https://stackoverflow.com/questions/60766292/how-to-get-keycloak-to-export-realm-users-and-then-exit
# docker-exec-cmd.sh

set -o errexit
set -o errtrace
set -o nounset
set -o pipefail

# If something goes wrong, this script does not run forever but times out
TIMEOUT_SECONDS=300
# Logfile for the keycloak export instance
LOGFILE=/tmp/standalone.sh.log
# destionation export file
JSON_EXPORT_FILE=/testdata/realm-export.json

rm -f ${LOGFILE} ${JSON_EXPORT_FILE}

# Start a new keycloak instance with exporting options enabled.
# Use prot offset to prevent port conflicts with the "real" keycloak instance.
timeout ${TIMEOUT_SECONDS}s \
/opt/jboss/keycloak/bin/standalone.sh \
-Dkeycloak.migration.action=export \
-Dkeycloak.migration.provider=singleFile \
-Dkeycloak.migration.realmName=master \
-Dkeycloak.migration.file=${JSON_EXPORT_FILE} \
-Dkeycloak.migration.usersExportStrategy=REALM_FILE \
-Djboss.socket.binding.port-offset=99 \
| tee -a ${LOGFILE} &

# Grab the keycloak export instance process id
PID="${!}"

# Wait for the export to finish
timeout ${TIMEOUT_SECONDS}s \
grep -m 1 "Export finished successfully" <(tail -f ${LOGFILE})

# Stop the keycloak export instance
kill ${PID}

1 change: 1 addition & 0 deletions cloudman/cloudman/tests/fixtures/keycloak-export.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker exec -it keycloak bash /testdata/keycloak-export-realm.sh
2 changes: 2 additions & 0 deletions cloudman/cloudman/tests/fixtures/keycloak-import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=testpassword -v `pwd`:/testdata/ -e JAVA_OPTS="-server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true -Dkeycloak.migration.action=import -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=/testdata/realm-export.json -Dkeycloak.migration.strategy=OVERWRITE_EXISTING" jboss/keycloak:7.0.0

Loading

0 comments on commit 2b15d28

Please sign in to comment.