Skip to content

Conversation

@pbleser-oc
Copy link
Contributor

@pbleser-oc pbleser-oc commented Dec 9, 2025

Description

Ongoing implementation of the Groupware backend service, which initially happened on its own branch but should now occur on the main branch.

Changes are almost exclusively on packages that are distinct to the Groupware backend, namely

  • pkg/jmap, pkg/jscalendar and pkg/jscontact: contain the implementation of the JMAP protocol and data models for Core, Mail, Contacts, Calendars, Blobs, ...
  • services/groupware: contains the Groupware backend service which is currently configured to not be started by default, and must be explicitly included in the START_ADDITIONAL_SERVICES property

Changes to common areas include:

  • devtools/deployments/opencloud_full: addition of a container of the Stalwart mail server which is used for Groupware functionality
  • pkg/structs: add more helper functions that are then used in pkg/jmap and services/groupware

Note that it is not fully functional yet and is going to be under continued and ongoing development along with its UI counterparts.

Specifically, the following aspects are lacking and only implemented in a skeletal way as a proof of concept:

  • API documentation: originally developed using go-swagger and redocly-cli, but which is having severe limitations and might be superseded by an ongoing implementation of a custom source code parser which renders OpenAPI
  • unit tests: very little coverage if at all, which is mostly due to most of the implementation of the Groupware backend doing relatively little logic and is mostly implementing the JMAP model
  • integration tests: some coverage exists that performs tests against a Stalwart container using testcontainers, but will be made more exhaustive
  • k6 black box tests: none implemented yet
  • system documentation: none implemented yet, too early to do so

The Groupware backend is not meant to be used productively yet.

…enLDAP container as a directory for user authentication
…ince 'admin' exists as a regular user in LDAP and thus won't have access to the administration
…80 directly to access the userinfo endpoint using HTTP since the certificates in traefik are self-signed and end up being rejected by Stalwart with no option to bypass the certificate check
…ernal authentication API for third party services such as Stalwart
 * primitive implementation to demonstrate how it could work, still to
   be considered WIP at best

 * add new dependency: MicahParks/jwkset and MicahParks/keyfunc to
   retrieve the JWK set from KeyCloak to verify the signature of the
   JWTs sent as part of Bearer authentication in the /auth API

 * (minor) opencloud/.../service.go: clean up a logging statement that
   was introduced earlier to hunt down why the auth-api service was not
   being started
 * refactored the models to be strongly typed with structs and mapstruct
   to decompose the dynamic parts of the JMAP payloads

 * externalized large JSON strings for tests into .json files under
   testdata/

 * added a couple of fantasy Graph groupware APIs to explore further
   options

 * added k6 scripts to test those graph/me/messages APIs, with a setup
   program to set up users in LDAP, fill their IMAP inbox, activate them
   in Stalwart, cleaning things up, etc...
…arlier implementation attempt, which also fixes compilation issues due to changes in main
 * after having decided that the Groupware API should be a standalone
   independent custom REST API that is using JMAP data models as much as
   possible,
 * removed Groupware APIs from the Graph service
 * moved Groupware implementation to the Groupware service, and
   refactored a few things accordingly
…d error handling in jmap and services/groupware
…ctory, and started Swagger documentation implementation
 * jmap/EmailCreate: add more attributes that were omitted: Headers,
   InReplyTo, References, Sender

 * add jmap GetEmailSubmissionStatus

 * improve email integration tests by adding a thorough test for email
   submission

 * jmap integration tests: provision principals and domains using the
   Stalwart Management API, switching from an in-memory to an internal
   directory
@pbleser-oc pbleser-oc self-assigned this Dec 9, 2025
@pbleser-oc pbleser-oc requested a review from butonic December 10, 2025 08:32
@pbleser-oc pbleser-oc marked this pull request as ready for review December 10, 2025 08:32

svc, err := NewAuthenticationApi(options.Config, &options.Logger, options.Metrics, options.TraceProvider, m)
if err != nil {
panic(err) // TODO p.bleser what to do when we encounter an error in a NewService() ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authentication api should be passed in as an option so the creation cannot fail here.

Run the following command on your host (requires the `ldap-tools` package with the `ldapsearch` CLI tool), which should output a list of DNs of demo users:

```bash
ldapsearch -h localhost -D 'cn=admin,dc=opencloud,dc=eu' \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only works with the url for me

Suggested change
ldapsearch -h localhost -D 'cn=admin,dc=opencloud,dc=eu' \
ldapsearch -H ldap://localhost -D 'cn=admin,dc=opencloud,dc=eu' \

curl -ks -D- -X POST \
"https://keycloak.opencloud.test/realms/openCloud/protocol/openid-connect/token" \
-d username=alan -d password=demo -d grant_type=password \
-d client_id=groupware -d scope=openid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when working from top to bottom this fails. using a client_id=web works ... so I need to add a client id for the goupware somewhere ...

to which one should receive the following response:

```java
A OK [CAPABILITY IMAP4rev2 ...] Authentication successful
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I need to dig into:

stalwart-1            | 2025-12-10T16:07:05Z TRACE Raw IMAP input received (imap.raw-input) listenerId = "imaptls", localPort = 993, remoteIp = 172.39.0.1, remotePort = 45034, size = 19, contents = "A LOGIN alan demo\r\n"
stalwart-1            | 2025-12-10T16:07:05Z ERROR LDAP error (store.ldap-error) listenerId = "imaptls", localPort = 993, remoteIp = 172.39.0.1, remotePort = 45034, reason = "I/O error: Connection refused (os error 111)", causedBy = "crates/directory/src/core/dispatch.rs:25", id = "A"

Comment on lines +313 to +314
STALWART_DOMAIN=

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
STALWART_DOMAIN=
STALWART_DOMAIN=
# LDAP config to use. Can either be idmldap (the built in IdP) or ldap (when using keycloak).
STALWART_AUTH_DIRECTORY=idmldap

## Services

### Stalwart

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a section about the STALWART_AUTH_DIRECTORY env var

@@ -0,0 +1,19 @@
package groupware
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should drop the groupware_ prefix from files in this package

@@ -0,0 +1,49 @@
package jmap
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should drop the jmap_ prefix from all files in this package

AUTH_BASIC_LDAP_BIND_PASSWORD: "admin"
USERS_LDAP_BIND_PASSWORD: "admin"
GROUPS_LDAP_BIND_PASSWORD: "admin"
IDM_LDAPS_ADDR: 0.0.0.0:9235
Copy link
Contributor

@butonic butonic Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I configured everything on .compose.test to prevent collisions with my .opencloud.test deployment. The stalwart URL then needs to be set for the groupware:

Suggested change
IDM_LDAPS_ADDR: 0.0.0.0:9235
IDM_LDAPS_ADDR: 0.0.0.0:9235
GROUPWARE_JMAP_BASE_URL: https://${STALWART_DOMAIN:-stalwart.opencloud.test}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh and we need to set FRONTEND_GROUPWARE_ENABLED: "true" and enable the mail app in the web config when stalwart is enabled ... but that connot be configured with a simple env var ... 😞 we need to replace the config file then ...

Copy link
Contributor

@butonic butonic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I still don't have a menu entry for mail in the web UI even though the config.json contains the mail app. I assume that is a problem with my setup/config. in any case, I can see the mail UI when navigation to {$OC_URL}/mail manually. \o/

I did run into some minor issues when following the docs. If you could address them (and enlighten me on how to get menu to show the groupware apps) I'm happy to merge this. I mostly want others to be able follow the DEVELOPMENT.md and have a working setup. Kudos for that, btw.

The groupware service itself follows our ... boilerplate ... service code ... and implements the JMAP handling. I assume that will have to evolve, but we can merge it and iterate.

Tip

the web repo has a compose file with all the apps enabled. That gave me the final hint to get the menu entries

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status:Needs-Review Type:Maintenance E.g. technical debt, packaging, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants