Skip to content

Commit

Permalink
Implement control panel to configure pas.plugins.oidc (#66)
Browse files Browse the repository at this point in the history
* Implement control panel to configure pas.plugins.oidc (Implements #65)

* Fix dependencies

* Use PLUGIN_ID instead of hardcoded value for oidc
  • Loading branch information
ericof authored Dec 10, 2024
1 parent b26bba1 commit c05ee2f
Show file tree
Hide file tree
Showing 22 changed files with 1,051 additions and 30 deletions.
1 change: 1 addition & 0 deletions news/65.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement control panel to configure pas.plugins.oidc [@ericof]
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@
"Zope",
"Products.CMFCore",
"plone.api",
"plone.app.registry",
"plone.base",
"plone.protect",
"plone.restapi>=8.34.0",
"oic",
"z3c.form",
],
extras_require={
"test": [
Expand Down
9 changes: 2 additions & 7 deletions src/pas/plugins/oidc/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<i18n:registerTranslations directory="locales" />

<include package=".browser" />
<include package=".controlpanel" />
<include package=".services" />

<genericsetup:registerProfile
Expand All @@ -40,13 +41,7 @@
post_handler=".setuphandlers.uninstall"
/>

<genericsetup:upgradeStep
title="Activate challenge plugin"
profile="pas.plugins.oidc:default"
source="1000"
destination="1001"
handler=".setuphandlers.activate_challenge_plugin"
/>
<include package=".upgrades" />

<utility
factory=".setuphandlers.HiddenProfiles"
Expand Down
Empty file.
166 changes: 166 additions & 0 deletions src/pas/plugins/oidc/controlpanel/classic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from pas.plugins.oidc import _
from pas.plugins.oidc import PLUGIN_ID
from pas.plugins.oidc.interfaces import IOIDCSettings
from plone import api
from plone.app.registry.browser import controlpanel
from plone.base.interfaces import IPloneSiteRoot
from zope.component import adapter
from zope.interface import implementer


@adapter(IPloneSiteRoot)
@implementer(IOIDCSettings)
class OIDCControlPanelAdapter:

def __init__(self, context):
self.context = context
self.portal = api.portal.get()
self.encoding = "utf-8"
self.settings = self.portal.acl_users[PLUGIN_ID]

@property
def issuer(self):
return self.settings.issuer

@issuer.setter
def issuer(self, value):
self.settings.issuer = value

@property
def client_id(self):
return self.settings.client_id

@client_id.setter
def client_id(self, value):
self.settings.client_id = value

@property
def client_secret(self):
return self.settings.client_secret

@client_secret.setter
def client_secret(self, value):
self.settings.client_secret = value

@property
def redirect_uris(self):
return self.settings.redirect_uris

@redirect_uris.setter
def redirect_uris(self, value):
self.settings.redirect_uris = value

@property
def use_session_data_manager(self):
return self.settings.use_session_data_manager

@use_session_data_manager.setter
def use_session_data_manager(self, value):
self.settings.use_session_data_manager = value

@property
def create_user(self):
return self.settings.create_user

@create_user.setter
def create_user(self, value):
self.settings.create_user = value

@property
def create_groups(self):
return self.settings.create_groups

@create_groups.setter
def create_groups(self, value):
self.settings.create_groups = value

@property
def user_property_as_groupid(self):
return self.settings.user_property_as_groupid

@user_property_as_groupid.setter
def user_property_as_groupid(self, value):
self.settings.user_property_as_groupid = value

@property
def create_ticket(self):
return self.settings.create_ticket

@create_ticket.setter
def create_ticket(self, value):
self.settings.create_ticket = value

@property
def create_restapi_ticket(self):
return self.settings.create_restapi_ticket

@create_restapi_ticket.setter
def create_restapi_ticket(self, value):
self.settings.create_restapi_ticket = value

@property
def scope(self):
return self.settings.scope

@scope.setter
def scope(self, value):
self.settings.scope = value

@property
def use_pkce(self):
return self.settings.use_pkce

@use_pkce.setter
def use_pkce(self, value):
self.settings.use_pkce = value

@property
def use_deprecated_redirect_uri_for_logout(self):
return self.settings.use_deprecated_redirect_uri_for_logout

@use_deprecated_redirect_uri_for_logout.setter
def use_deprecated_redirect_uri_for_logout(self, value):
self.settings.use_deprecated_redirect_uri_for_logout = value

@property
def use_modified_openid_schema(self):
return self.settings.use_modified_openid_schema

@use_modified_openid_schema.setter
def use_modified_openid_schema(self, value):
self.settings.use_modified_openid_schema = value

@property
def user_property_as_userid(self):
return self.settings.user_property_as_userid

@user_property_as_userid.setter
def user_property_as_userid(self, value):
self.settings.user_property_as_userid = value


class OIDCSettingsForm(controlpanel.RegistryEditForm):
schema = IOIDCSettings
schema_prefix = "oidc_admin"
label = _("OIDC Plugin Settings")
description = ""

def getContent(self):
portal = api.portal.get()
return OIDCControlPanelAdapter(portal)

def applyChanges(self, data):
"""See interfaces.IEditForm"""
content = self.getContent()
changes = {}
for name in data:
current = getattr(content, name)
value = data[name]
if current != value:
setattr(content, name, value)
changes.setdefault(IOIDCSettings, []).append(name)
return changes


class OIDCSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
form = OIDCSettingsForm
27 changes: 27 additions & 0 deletions src/pas/plugins/oidc/controlpanel/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="pas.plugins.oidc"
>

<!-- ClassicUI Control panel -->
<adapter factory=".classic.OIDCControlPanelAdapter" />
<browser:page
name="oidc-controlpanel"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
class=".classic.OIDCSettingsControlPanel"
permission="cmf.ManagePortal"
layer="pas.plugins.oidc.interfaces.IDefaultBrowserLayer"
/>

<!-- Restapi Controlpanel -->
<adapter factory=".serializer.OIDCControlpanelSerializeToJson" />
<adapter factory=".deserializer.OIDCControlpanelDeserializeFromJson" />

<adapter
factory=".oidc.OIDCSettingsConfigletPanel"
provides="pas.plugins.oidc.interfaces.IOIDCControlpanel"
name="oidc_admin"
/>

</configure>
80 changes: 80 additions & 0 deletions src/pas/plugins/oidc/controlpanel/deserializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from pas.plugins.oidc import PLUGIN_ID
from pas.plugins.oidc.interfaces import IOIDCControlpanel
from plone import api
from plone.restapi.deserializer import json_body
from plone.restapi.deserializer.controlpanels import ControlpanelDeserializeFromJson
from plone.restapi.deserializer.controlpanels import FakeDXContext
from plone.restapi.interfaces import IDeserializeFromJson
from plone.restapi.interfaces import IFieldDeserializer
from z3c.form.interfaces import IManagerValidator
from zExceptions import BadRequest
from zope.component import adapter
from zope.component import queryMultiAdapter
from zope.interface import implementer
from zope.interface.exceptions import Invalid
from zope.schema import getFields
from zope.schema.interfaces import ValidationError


@implementer(IDeserializeFromJson)
@adapter(IOIDCControlpanel)
class OIDCControlpanelDeserializeFromJson(ControlpanelDeserializeFromJson):
@property
def proxy(self):
portal = api.portal.get()
plugin = portal.acl_users[PLUGIN_ID]
return plugin

def __call__(self, mask_validation_errors=True):
controlpanel = self.controlpanel
request = controlpanel.request
data = json_body(request)

proxy = self.proxy

schema_data = {}
errors = []

# Make a fake context
fake_context = FakeDXContext()

for name, field in getFields(self.schema).items():
field_data = schema_data.setdefault(self.schema, {})

if field.readonly:
continue

if name in data:
deserializer = queryMultiAdapter(
(field, fake_context, request), IFieldDeserializer
)
try:
# Make it sane
value = deserializer(data[name])
# Validate required etc
field.validate(value)
# Set the value.
setattr(proxy, name, value)
except ValidationError as e:
errors.append({"message": e.doc(), "field": name, "error": e})
except (ValueError, Invalid) as e:
errors.append({"message": str(e), "field": name, "error": e})
else:
field_data[name] = value

# Validate schemata
for schema, field_data in schema_data.items():
validator = queryMultiAdapter(
(self.context, request, None, schema, None), IManagerValidator
)
for error in validator.validate(field_data):
errors.append({"error": error, "message": str(error)})

if errors:
for error in errors:
if mask_validation_errors:
# Drop Python specific error classes in order to
# be able to better handle errors on front-end
error["error"] = "ValidationError"
error["message"] = api.env.translate(error["message"], context=request)
raise BadRequest(errors)
20 changes: 20 additions & 0 deletions src/pas/plugins/oidc/controlpanel/oidc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pas.plugins.oidc import _
from pas.plugins.oidc.interfaces import IDefaultBrowserLayer
from pas.plugins.oidc.interfaces import IOIDCControlpanel
from pas.plugins.oidc.interfaces import IOIDCSettings
from plone.restapi.controlpanels import RegistryConfigletPanel
from zope.component import adapter
from zope.interface import implementer
from zope.interface import Interface


@adapter(Interface, IDefaultBrowserLayer)
@implementer(IOIDCControlpanel)
class OIDCSettingsConfigletPanel(RegistryConfigletPanel):
"""Control Panel endpoint"""

schema = IOIDCSettings
configlet_id = "oidc_admin"
configlet_category_id = "plone-users"
title = _("OIDC settings")
group = ""
45 changes: 45 additions & 0 deletions src/pas/plugins/oidc/controlpanel/serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from pas.plugins.oidc import PLUGIN_ID
from pas.plugins.oidc.interfaces import IOIDCControlpanel
from pas.plugins.oidc.plugins import OIDCPlugin
from plone import api
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.serializer.controlpanels import ControlpanelSerializeToJson
from plone.restapi.serializer.controlpanels import get_jsonschema_for_controlpanel
from plone.restapi.serializer.controlpanels import SERVICE_ID
from zope.component import adapter
from zope.interface import implementer


@implementer(ISerializeToJson)
@adapter(IOIDCControlpanel)
class OIDCControlpanelSerializeToJson(ControlpanelSerializeToJson):
def config_data(self) -> dict:
data = {}
portal = api.portal.get()
plugin = portal.acl_users[PLUGIN_ID]
properties = OIDCPlugin._properties
for prop in properties:
key = prop["id"]
data[key] = getattr(plugin, key, "")
return data

def __call__(self):
json_data = self.config_data()
controlpanel = self.controlpanel
context = controlpanel.context
request = controlpanel.request
url = context.absolute_url()
json_schema = get_jsonschema_for_controlpanel(
controlpanel,
context,
request,
)
response = {
"@id": f"{url}/{SERVICE_ID}/{controlpanel.__name__}",
"title": "OIDC settings",
"description": "Configure OIDC connection strings",
"group": controlpanel.group,
"data": json_data,
"schema": json_schema,
}
return response
Loading

0 comments on commit c05ee2f

Please sign in to comment.