Add OIDCOAuth config to Apache.

This change introduces the configuration of OAuth to enable auth-openidc
which is browser-less.

When enable-oauth is set to true (the default) and
oidc-auth-verify-jwks-uri is empty, the charm will try use
oidc-oauth-introspection-endpoint if set, otherwise the charm will fetch
the content at oidc-provider-metadata-url and use the value available at
the key introspection_endpoint.
This commit is contained in:
Felipe Reyes 2022-08-16 10:44:26 -04:00
parent 4b8a2541eb
commit f93b4451fb
4 changed files with 136 additions and 34 deletions

View File

@ -85,3 +85,26 @@ options:
description: |
The claim that is used when setting the REMOTE_USER variable on OpenID
Connect protected paths, for example: email.
oidc-provider-jwks-uri:
default: ''
type: string
description: |
.
enable-oauth:
default: true
type: boolean
description: |
Set to true to enable OAuth2 support.
oidc-oauth-verify-jwks-uri:
default: ''
type: string
description: |
The JWKs URL on which the Authorization Server publishes the keys used
to sign its JWT access tokens.
oidc-oauth-introspection-endpoint:
default: ''
type: string
description: |
OAuth 2.0 Authorization Server token introspection endpoint. When
`enable-oauth` is set to true and this option unset (the default), the
introspection endpoint available in the metadata will be used.

View File

@ -1,2 +1,3 @@
ops>=1.5.0
git+https://opendev.org/openstack/charm-ops-openstack@master#egg=ops_openstack
requests

View File

@ -22,11 +22,12 @@ import subprocess
from typing import List
from uuid import uuid4
import ops_openstack.core
import requests
from ops.main import main
from ops.model import StatusBase, ActiveStatus, BlockedStatus
import ops_openstack.core
from ops_openstack.adapters import (
ConfigurationAdapter,
)
@ -61,7 +62,7 @@ class KeystoneOpenIDCOptions(ConfigurationAdapter):
@property
def hostname(self) -> str:
"""Hostname as advertised by the principal charm"""
"""Hostname as advertised by the principal charm."""
data = self._get_principal_data()
try:
return json.loads(data['hostname'])
@ -70,6 +71,7 @@ class KeystoneOpenIDCOptions(ConfigurationAdapter):
@property
def openidc_location_config(self) -> str:
"""Path to the file with the OpenID Connect configuration."""
return os.path.join(self.charm_instance.config_dir,
f'openidc-location.{self.idp_id}.conf')
@ -81,7 +83,7 @@ class KeystoneOpenIDCOptions(ConfigurationAdapter):
@property
def idp_id(self) -> str:
return self.charm_instance.unit.app.name
return 'openid'
@property
def scheme(self) -> str:
@ -115,7 +117,24 @@ class KeystoneOpenIDCOptions(ConfigurationAdapter):
logger.debug('Using oidc-crypto-passphrase from app databag')
return crypto_passphrase
else:
logger.warn('The oidc-crypto-passphrase has not been set')
logger.warning('The oidc-crypto-passphrase has not been set')
return None
@property
def metadata(self):
"""Metadata content offered by the Identity Provider.
The content available at the url configured in
oidc-provider-metadata-url is read and parsed as json.
"""
if self.oidc_provider_metadata_url:
logging.info('GETing content from %s',
self.oidc_provider_metadata_url)
r = requests.get(self.oidc_provider_metadata_url)
return r.json()
else:
logging.info('Metadata was not retrieved since '
'oidc-provider-metadata-url is not set')
return None
@ -126,6 +145,8 @@ class KeystoneOpenIDCCharm(ops_openstack.core.OSBaseCharm):
REQUIRED_RELATIONS = ['keystone-fid-service-provider',
'websso-fid-service-provider']
REQUIRED_KEYS = ['oidc_crypto_passphrase', 'oidc_client_id',
'hostname', 'port', 'scheme']
APACHE2_MODULE = 'auth_openidc'
CONFIG_FILE_OWNER = 'root'
@ -133,7 +154,7 @@ class KeystoneOpenIDCCharm(ops_openstack.core.OSBaseCharm):
release = 'xena' # First release supported.
auth_method = 'mapped' # the driver to be used.
auth_method = 'openid' # the driver to be used.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -168,14 +189,22 @@ class KeystoneOpenIDCCharm(ops_openstack.core.OSBaseCharm):
)
# Event handlers
# Extending the default handler for install hook to enable the apache2
# openidc module.
def on_install(self, _):
"""Install hook handler.
This event handler installs the list of packages defined in the
property PACKAGES and enables the openidc apache module.
"""
super().on_install(_)
self.enable_module()
def _on_start(self, _):
"""Start hook handler.
Set the flag `is_started` which is consumed by the update-status
hook. This charm doesn't run new services, so there is no need to
start anything.
"""
self._stored.is_started = True
def _on_keystone_fid_service_provider_relation_joined(self, event):
@ -189,7 +218,9 @@ class KeystoneOpenIDCCharm(ops_openstack.core.OSBaseCharm):
relation = self.model.get_relation('keystone-fid-service-provider')
data = relation.data[self.unit]
data['auth-method'] = json.dumps(self.auth_method)
# When (if) this patch is merged, we can use auth-method
# https://review.opendev.org/c/openstack/charm-keystone/+/852601
# data['auth-method'] = json.dumps(self.auth_method)
data['protocol-name'] = json.dumps(self.options.idp_id)
data['remote-id-attribute'] = json.dumps(
self.options.remote_id_attribute)
@ -230,39 +261,37 @@ class KeystoneOpenIDCCharm(ops_openstack.core.OSBaseCharm):
for relation in relations:
data = relation.data[self.unit.app]
break
logger.info('Generating oidc-client-secret')
client_secret = str(uuid4())
data.update({'oidc-client-secret': client_secret})
logger.info('Generating oidc-crypto-passphrase')
data.update({'oidc-crypto-passphrase': str(uuid4())})
else:
logger.debug('Not leader, skipping oidc-client-secret generation')
logger.debug('Not leader, skipping oidc-crypto-passphrase '
'generation')
def _on_cluster_relation_changed(self, _):
self._on_config_changed(_)
# properties
@property
def restart_map(self):
return {self.options.openidc_location_config: ['apache2']}
@property
def restart_functions(self):
return {'apache2': self.request_restart}
def is_data_ready(self) -> bool:
if not self.model.get_relation('cluster'):
return False
return len(self.find_missing_keys()) == 0
def find_missing_keys(self) -> List[str]:
"""Find keys not set that are needed for the charm to work correctly.
:returns: List of configuration keys that need to be set and are not.
"""
options = KeystoneOpenIDCOptions(self)
required_keys = ['oidc_crypto_passphrase', 'oidc_client_id',
'hostname', 'port', 'scheme']
missing_keys = []
for key in required_keys:
for key in self.REQUIRED_KEYS:
if getattr(options, key) in [None, '']:
missing_keys.append(key)
if missing_keys:
logger.debug('Incomplete data: %s', ' '.join(missing_keys))
return len(missing_keys) == 0
return missing_keys
def services(self) -> List[str]:
"""Determine the list of services that should be running."""
@ -275,19 +304,24 @@ class KeystoneOpenIDCCharm(ops_openstack.core.OSBaseCharm):
return BlockedStatus('incomplete data')
def enable_module(self):
logger.info(f'Enabling apache2 module: {self.APACHE2_MODULE}')
"""Enable oidc Apache module."""
logger.info('Enabling apache2 module: %s', self.APACHE2_MODULE)
subprocess.check_call(['a2enmod', self.APACHE2_MODULE])
def disable_module(self):
logger.info(f'Disabling apache2 module: {self.APACHE2_MODULE}')
"""Disable oidc Apache module."""
logger.info('Disabling apache2 module: %s', self.APACHE2_MODULE)
subprocess.check_call(['a2dismod', self.APACHE2_MODULE])
def request_restart(self, service_name=None):
"""Request a restart of the service to the principal."""
"""Request a restart of the service to the principal.
:param service_name: name of the service to restart, but unused.
"""
relation = self.model.get_relation('keystone-fid-service-provider')
data = relation.data[self.unit]
logger.debug('Requesting a restart to the principal charm')
logger.info('Requesting a restart to the principal charm')
data['restart-nonce'] = json.dumps(str(uuid4()))
def render_config(self):
@ -307,6 +341,15 @@ class KeystoneOpenIDCCharm(ops_openstack.core.OSBaseCharm):
perms=0o440
)
# properties
@property
def restart_map(self):
return {self.options.openidc_location_config: ['apache2']}
@property
def restart_functions(self):
return {'apache2': self.request_restart}
@property
def config_dir(self):
return CONFIG_DIR

View File

@ -33,13 +33,48 @@ OIDCCryptoPassphrase {{ options.oidc_crypto_passphrase }}
OIDCRedirectURI {{ options.scheme }}://{{ options.hostname }}:{{ options.port }}/v3/OS-FEDERATION/identity_providers/{{ options.idp_id }}/protocols/{{ options.protocol_id }}/auth
{% if options.oidc_remote_user_claim -%}
OICDRemoteUserClaim {{ options.oidc_remote_user_claim }}
OIDCRemoteUserClaim {{ options.oidc_remote_user_claim }}
{% endif -%}
{% if options.oidc_provider_jwks_uri -%}
OIDCProviderJwksUri {{ options.oidc_provider_jwks_uri }}
{% endif -%}
{%- if options.enable_oauth %}
{%- if options.oidc_auth_verify_jwks_uri %}
OIDCOAuthVerifyJwksUri {{ options.oidc_auth_verify_jwks_uri }}
{%- else %}
OIDCOAuthIntrospectionEndpoint {{ options.oidc_oauth_introspection_endpoint|default(options.provider_metadata.introspection_endpoint) }}
OIDCOAuthIntrospectionEndpointParams token_type_hint=access_token
OIDCOAuthClientID {{ options.oidc_client_id }}
{%- if options.oidc_client_secret %}
OIDCOAuthClientSecret {{ options.oidc_client_secret }}
{%- endif %}
{%- endif %}
{%- endif %}
<LocationMatch /v3/OS-FEDERATION/identity_providers/{{ options.idp_id }}/protocols/{{ options.protocol_id }}/auth>
AuthType {{ options.auth_type }}
Require valid-user
{% if options.debug -%}
{%- if options.debug %}
LogLevel debug
{% endif -%}
{%- endif %}
</LocationMatch>
# Support for websso from Horizon
OIDCRedirectURI "{{ options.scheme }}://{{ options.hostname }}:{{ options.port }}/v3/auth/OS-FEDERATION/identity_providers/{{ options.idp_id }}/protocols/{{ options.protocol_id }}/websso"
OIDCRedirectURI "{{ options.scheme }}://{{ options.hostname }}:{{ options.port }}/v3/auth/OS-FEDERATION/websso/{{ options.protocol_id }}"
<Location /v3/auth/OS-FEDERATION/websso/{{ options.protocol_id }}>
Require valid-user
AuthType openid-connect
{%- if options.debug %}
LogLevel debug
{%- endif %}
</Location>
<Location /v3/auth/OS-FEDERATION/identity_providers/{{ options.idp_id }}/protocols/{{ options.protocol_id }}/websso>
Require valid-user
AuthType openid-connect
{%- if options.debug %}
LogLevel debug
{%- endif %}
</Location>