Skip to content
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

Implement control panel to configure pas.plugins.oidc #66

Merged
merged 3 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading