Files
opencloud/pkg/jmap/jmap_session.go
Pascal Bleser d0c9358908 groupware: session handling improvements
* remove the baseurl from the JMAP client configuration, and pass it to
   the session retrieval functions instead, as that is really the only
   place where it is relevant, and we gain flexibility to discover that
   session URL differently in the future without having to touch the
   JMAP client

 * move the default account identifier handling from the JMAP package to
   the Groupware one, as it really has nothing to do with JMAP itself,
   and is an opinionated feature of the Groupware REST API instead

 * add an event listener interface for JMAP events to be more flexible
   and universal, typically for metrics that are defined on the API
   level that uses the JMAP client

 * add errors for when default accounts cannot be determined

 * split groupware_framework.go into groupware_framework.go,
   groupware_request.go and groupware_response.go

 * move the accountId logging into the Groupware level instead of JMAP
   since it can also be relevant to other operations that might be
   worthy of logging before the JMAP client is even invoked
2026-04-30 10:51:41 +02:00

128 lines
4.6 KiB
Go

package jmap
import (
"errors"
"fmt"
"net/url"
"github.com/opencloud-eu/opencloud/pkg/log"
)
type SessionEventListener interface {
OnSessionOutdated(session *Session, newSessionState SessionState)
}
// Cached user related information
//
// This information is typically retrieved once (or at least for a certain period of time) from the
// JMAP well-known endpoint of Stalwart and then kept in cache to avoid the performance cost of
// retrieving it over and over again.
//
// This is really only needed due to the Graph API limitations, since ideally, the account ID should
// be passed as a request parameter by the UI, in order to support a user having multiple accounts.
//
// Keeping track of the JMAP URL might be useful though, in case of Stalwart sharding strategies making
// use of that, by providing different URLs for JMAP on a per-user basis, and that is not something
// we would want to query before every single JMAP request. On the other hand, that then also creates
// a risk of going out-of-sync, e.g. if a node is down and the user is reassigned to a different node.
// There might be webhooks to subscribe to in Stalwart to be notified of such situations, in which case
// the Session needs to be removed from the cache.
//
// The Username is only here for convenience, it could just as well be passed as a separate parameter
// instead of being part of the Session, since the username is always part of the request (typically in
// the authentication token payload.)
type Session struct {
// The name of the user to use to authenticate against Stalwart
Username string
// The base URL to use for JMAP operations towards Stalwart
JmapUrl url.URL
// An identifier of the JmapUrl to use in metrics and tracing
JmapEndpoint string
// The upload URL template
UploadUrlTemplate string
// An identifier of the UploadUrlTemplate to use in metrics and tracing
UploadEndpoint string
// The upload URL template
DownloadUrlTemplate string
// An identifier of the DownloadUrlTemplate to use in metrics and tracing
DownloadEndpoint string
SessionResponse
}
var (
invalidSessionResponseErrorMissingUsername = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide a username")}
invalidSessionResponseErrorMissingApiUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide an API URL")}
invalidSessionResponseErrorInvalidApiUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response provides an invalid API URL")}
invalidSessionResponseErrorMissingUploadUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide an upload URL")}
invalidSessionResponseErrorMissingDownloadUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide a download URL")}
)
// Create a new Session from a SessionResponse.
func newSession(sessionResponse SessionResponse) (Session, Error) {
username := sessionResponse.Username
if username == "" {
return Session{}, invalidSessionResponseErrorMissingUsername
}
apiStr := sessionResponse.ApiUrl
if apiStr == "" {
return Session{}, invalidSessionResponseErrorMissingApiUrl
}
apiUrl, err := url.Parse(apiStr)
if err != nil {
return Session{}, invalidSessionResponseErrorInvalidApiUrl
}
apiEndpoint := endpointOf(apiUrl)
uploadUrl := sessionResponse.UploadUrl
if uploadUrl == "" {
return Session{}, invalidSessionResponseErrorMissingUploadUrl
}
uploadEndpoint := toEndpoint(uploadUrl)
downloadUrl := sessionResponse.DownloadUrl
if downloadUrl == "" {
return Session{}, invalidSessionResponseErrorMissingDownloadUrl
}
downloadEndpoint := toEndpoint(downloadUrl)
return Session{
Username: username,
JmapUrl: *apiUrl,
JmapEndpoint: apiEndpoint,
UploadUrlTemplate: uploadUrl,
UploadEndpoint: uploadEndpoint,
DownloadUrlTemplate: downloadUrl,
DownloadEndpoint: downloadEndpoint,
SessionResponse: sessionResponse,
}, nil
}
// Create a new log.Logger that is decorated with fields containing information about the Session.
func (s Session) DecorateLogger(l log.Logger) *log.Logger {
return log.From(l.With().
Str(logUsername, log.SafeString(s.Username)).
Str(logEndpoint, log.SafeString(s.JmapEndpoint)).
Str(logSessionState, log.SafeString(string(s.State))))
}
func endpointOf(u *url.URL) string {
if u != nil {
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
} else {
return ""
}
}
func toEndpoint(str string) string {
u, err := url.Parse(str)
if err == nil {
return endpointOf(u)
} else {
return str
}
}