From 0247c28d5859d140dfdc583aef2eeffeeb0f78e6 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Fri, 25 Jul 2025 15:19:46 +0200 Subject: [PATCH] Refactor groupware service after ADR decision on the Groupware API * 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 --- .../config/stalwart/config.toml | 10 +- .../deployments/opencloud_full/stalwart.yml | 1 + pkg/jmap/jmap.go | 120 ++++++-- pkg/jmap/jmap_http.go | 57 ++-- pkg/jmap/jmap_model.go | 143 +++++---- pkg/jmap/jmap_reva.go | 32 -- pkg/jmap/jmap_test.go | 6 +- services/graph/pkg/config/config.go | 16 - .../pkg/config/defaults/defaultconfig.go | 12 - .../pkg/service/v0/groupware/groupware.go | 237 --------------- .../service/v0/groupware/groupware_test.go | 51 ---- .../service/v0/groupware/groupware_tools.go | 176 ----------- services/graph/pkg/service/v0/service.go | 6 - services/groupware/pkg/config/config.go | 20 +- .../pkg/config/defaults/defaultconfig.go | 27 +- services/groupware/pkg/config/parser/parse.go | 7 +- services/groupware/pkg/config/reva.go | 6 + services/groupware/pkg/groupware/groupware.go | 162 ++++++++++ .../pkg/groupware/groupware_lowlevel.go | 282 ++++++++++++++++++ .../groupware/pkg/groupware/groupware_reva.go | 36 +++ .../pkg/groupware/groupware_tools.go | 30 ++ services/groupware/pkg/middleware/auth.go | 78 +++++ services/groupware/pkg/server/http/server.go | 11 +- .../groupware/pkg/service/http/v0/service.go | 93 +----- .../pkg/config/defaults/defaultconfig.go | 5 +- 25 files changed, 861 insertions(+), 763 deletions(-) delete mode 100644 pkg/jmap/jmap_reva.go delete mode 100644 services/graph/pkg/service/v0/groupware/groupware.go delete mode 100644 services/graph/pkg/service/v0/groupware/groupware_test.go delete mode 100644 services/graph/pkg/service/v0/groupware/groupware_tools.go create mode 100644 services/groupware/pkg/config/reva.go create mode 100644 services/groupware/pkg/groupware/groupware.go create mode 100644 services/groupware/pkg/groupware/groupware_lowlevel.go create mode 100644 services/groupware/pkg/groupware/groupware_reva.go create mode 100644 services/groupware/pkg/groupware/groupware_tools.go create mode 100644 services/groupware/pkg/middleware/auth.go diff --git a/devtools/deployments/opencloud_full/config/stalwart/config.toml b/devtools/deployments/opencloud_full/config/stalwart/config.toml index 0441b5d50..44c452e90 100644 --- a/devtools/deployments/opencloud_full/config/stalwart/config.toml +++ b/devtools/deployments/opencloud_full/config/stalwart/config.toml @@ -39,11 +39,11 @@ metrics.prometheus.auth.secret = "secret" metrics.prometheus.auth.username = "metrics" metrics.prometheus.enable = true server.hostname = "stalwart.opencloud.test" -server.http.allowed-endpoint = 200 -server.http.hsts = false -server.http.permissive-cors = false -server.http.url = "'https://stalwart.opencloud.test:' + local_port" -server.http.use-x-forwarded = false +http.allowed-endpoint = 200 +http.hsts = true +http.permissive-cors = false +http.url = "'https://' + config_get('server.hostname')" +http.use-x-forwarded = true server.listener.http.bind = "[::]:8080" server.listener.http.protocol = "http" server.listener.https.bind = "[::]:443" diff --git a/devtools/deployments/opencloud_full/stalwart.yml b/devtools/deployments/opencloud_full/stalwart.yml index c688c9cf1..62fd9616d 100644 --- a/devtools/deployments/opencloud_full/stalwart.yml +++ b/devtools/deployments/opencloud_full/stalwart.yml @@ -14,6 +14,7 @@ services: - "127.0.0.1:143:143" - "127.0.0.1:993:993" volumes: + - /etc/localtime:/etc/localtime:ro - ./config/stalwart:/opt/stalwart/etc - stalwart-data:/opt/stalwart/data - stalwart-logs:/opt/stalwart/logs diff --git a/pkg/jmap/jmap.go b/pkg/jmap/jmap.go index f3e0d8d88..b74913942 100644 --- a/pkg/jmap/jmap.go +++ b/pkg/jmap/jmap.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "net/url" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/rs/zerolog" @@ -46,12 +47,9 @@ func NewClient(wellKnown WellKnownClient, api ApiClient) Client { // 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 identifier of the account to use when performing JMAP operations with Stalwart - AccountId string - // The base URL to use for JMAP operations towards Stalwart - JmapUrl string + Username string // The name of the user to use to authenticate against Stalwart + AccountId string // The identifier of the account to use when performing JMAP operations with Stalwart + JmapUrl url.URL // The base URL to use for JMAP operations towards Stalwart } const ( @@ -87,6 +85,18 @@ var ( errWellKnownResponseHasNoApiUrl = fmt.Errorf("well-known response has no API URL") ) +type WellKnownResponseHasInvalidApiUrlError struct { + ApiUrl string + Err error +} + +func (e WellKnownResponseHasInvalidApiUrlError) Error() string { + return fmt.Sprintf("well-known response contains an invalid API URL '%s': %v", e.ApiUrl, e.Err.Error()) +} +func (e WellKnownResponseHasInvalidApiUrlError) Unwrap() error { + return e.Err +} + // Create a new Session from a WellKnownResponse. func NewSession(wellKnownResponse WellKnownResponse) (Session, error) { username := wellKnownResponse.Username @@ -97,14 +107,18 @@ func NewSession(wellKnownResponse WellKnownResponse) (Session, error) { if accountId == "" { return Session{}, errWellKnownResponseHasJmapMailPrimaryAccount } - apiUrl := wellKnownResponse.ApiUrl - if apiUrl == "" { + apiStr := wellKnownResponse.ApiUrl + if apiStr == "" { return Session{}, errWellKnownResponseHasNoApiUrl } + apiUrl, err := url.Parse(apiStr) + if err != nil { + return Session{}, WellKnownResponseHasInvalidApiUrlError{ApiUrl: apiStr, Err: err} + } return Session{ Username: username, AccountId: accountId, - JmapUrl: apiUrl, + JmapUrl: *apiUrl, }, nil } @@ -172,31 +186,85 @@ func (j *Client) GetAllMailboxes(session *Session, ctx context.Context, logger * return j.GetMailbox(session, ctx, logger, nil) } +// https://jmap.io/spec-mail.html#mailboxquery +func (j *Client) QueryMailbox(session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterCondition) (MailboxQueryResponse, error) { + logger = j.logger("QueryMailbox", session, logger) + cmd, err := request(invocation(MailboxQuery, SimpleMailboxQueryCommand{AccountId: session.AccountId, Filter: filter}, "0")) + if err != nil { + return MailboxQueryResponse{}, err + } + return command(j.api, logger, ctx, session, cmd, func(body *Response) (MailboxQueryResponse, error) { + var response MailboxQueryResponse + err = retrieveResponseMatchParameters(body, MailboxQuery, "0", &response) + return response, err + }) +} + +type Mailboxes struct { + Mailboxes []Mailbox `json:"mailboxes,omitempty"` + State string `json:"state,omitempty"` +} + +func (j *Client) SearchMailboxes(session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterCondition) (Mailboxes, error) { + logger = j.logger("SearchMailboxes", session, logger) + + cmd, err := request( + invocation(MailboxQuery, SimpleMailboxQueryCommand{AccountId: session.AccountId, Filter: filter}, "0"), + invocation(MailboxGet, MailboxGetRefCommand{ + AccountId: session.AccountId, + IdRef: &Ref{Name: MailboxQuery, Path: "/ids/*", ResultOf: "0"}, + }, "1"), + ) + if err != nil { + return Mailboxes{}, err + } + + return command(j.api, logger, ctx, session, cmd, func(body *Response) (Mailboxes, error) { + var response MailboxGetResponse + err = retrieveResponseMatchParameters(body, MailboxGet, "1", &response) + if err != nil { + return Mailboxes{}, err + } + return Mailboxes{Mailboxes: response.List, State: body.SessionState}, nil + }) +} + type Emails struct { - Emails []Email - State string + Emails []Email `json:"emails,omitempty"` + State string `json:"state,omitempty"` } func (j *Client) GetEmails(session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (Emails, error) { logger = j.loggerParams("GetEmails", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Int(logOffset, offset).Int(logLimit, limit) }) + + query := EmailQueryCommand{ + AccountId: session.AccountId, + Filter: &MessageFilter{InMailbox: mailboxId}, + Sort: []Sort{{Property: emailSortByReceivedAt, IsAscending: false}}, + CollapseThreads: true, + CalculateTotal: false, + } + if offset >= 0 { + query.Position = offset + } + if limit >= 0 { + query.Limit = limit + } + + get := EmailGetRefCommand{ + AccountId: session.AccountId, + FetchAllBodyValues: fetchBodies, + IdRef: &Ref{Name: EmailQuery, Path: "/ids/*", ResultOf: "0"}, + } + if maxBodyValueBytes >= 0 { + get.MaxBodyValueBytes = maxBodyValueBytes + } + cmd, err := request( - invocation(EmailQuery, EmailQueryCommand{ - AccountId: session.AccountId, - Filter: &Filter{InMailbox: mailboxId}, - Sort: []Sort{{Property: emailSortByReceivedAt, IsAscending: false}}, - CollapseThreads: true, - Position: offset, - Limit: limit, - CalculateTotal: false, - }, "0"), - invocation(EmailGet, EmailGetCommand{ - AccountId: session.AccountId, - FetchAllBodyValues: fetchBodies, - MaxBodyValueBytes: maxBodyValueBytes, - IdRef: &Ref{Name: EmailQuery, Path: "/ids/*", ResultOf: "0"}, - }, "1"), + invocation(EmailQuery, query, "0"), + invocation(EmailGet, get, "1"), ) if err != nil { return Emails{}, err diff --git a/pkg/jmap/jmap_http.go b/pkg/jmap/jmap_http.go index f7e52e0d7..d1fbf6c55 100644 --- a/pkg/jmap/jmap_http.go +++ b/pkg/jmap/jmap_http.go @@ -7,25 +7,18 @@ import ( "fmt" "io" "net/http" - "path" + "net/url" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/version" ) -type HttpJmapUsernameProvider interface { - // Provide the username for JMAP operations. - GetUsername(req *http.Request, ctx context.Context, logger *log.Logger) (string, error) -} - type HttpJmapApiClient struct { - baseurl string - jmapurl string - client *http.Client - usernameProvider HttpJmapUsernameProvider - masterUser string - masterPassword string - userAgent string + baseurl url.URL + client *http.Client + masterUser string + masterPassword string + userAgent string } var ( @@ -39,15 +32,13 @@ func bearer(req *http.Request, token string) { } */ -func NewHttpJmapApiClient(baseurl string, jmapurl string, client *http.Client, usernameProvider HttpJmapUsernameProvider, masterUser string, masterPassword string) *HttpJmapApiClient { +func NewHttpJmapApiClient(baseurl url.URL, client *http.Client, masterUser string, masterPassword string) *HttpJmapApiClient { return &HttpJmapApiClient{ - baseurl: baseurl, - jmapurl: jmapurl, - client: client, - usernameProvider: usernameProvider, - masterUser: masterUser, - masterPassword: masterPassword, - userAgent: "OpenCloud/" + version.GetString(), + baseurl: baseurl, + client: client, + masterUser: masterUser, + masterPassword: masterPassword, + userAgent: "OpenCloud/" + version.GetString(), } } @@ -67,18 +58,7 @@ func (e AuthenticationError) Unwrap() error { return e.Err } -func (h *HttpJmapApiClient) auth(logger *log.Logger, ctx context.Context, req *http.Request) error { - username, err := h.usernameProvider.GetUsername(req, ctx, logger) - if err != nil { - logger.Error().Err(err).Msg("failed to find username") - return AuthenticationError{Err: err} - } - masterUsername := username + "%" + h.masterUser - req.SetBasicAuth(masterUsername, h.masterPassword) - return nil -} - -func (h *HttpJmapApiClient) authWithUsername(_ *log.Logger, username string, req *http.Request) error { +func (h *HttpJmapApiClient) auth(username string, logger *log.Logger, req *http.Request) error { masterUsername := username + "%" + h.masterUser req.SetBasicAuth(masterUsername, h.masterPassword) return nil @@ -100,14 +80,14 @@ func (e HttpError) Unwrap() error { } func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (WellKnownResponse, error) { - wellKnownUrl := path.Join(h.baseurl, ".well-known", "jmap") + wellKnownUrl := h.baseurl.JoinPath(".well-known", "jmap").String() req, err := http.NewRequest(http.MethodGet, wellKnownUrl, nil) if err != nil { logger.Error().Err(err).Msgf("failed to create GET request for %v", wellKnownUrl) return WellKnownResponse{}, HttpError{Op: "creating request", Method: http.MethodGet, Url: wellKnownUrl, Username: username, Err: err} } - h.authWithUsername(logger, username, req) + h.auth(username, logger, req) req.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate") // spec recommendation res, err := h.client.Do(req) @@ -145,10 +125,7 @@ func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (W } func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, error) { - jmapUrl := h.jmapurl - if jmapUrl == "" { - jmapUrl = session.JmapUrl - } + jmapUrl := session.JmapUrl.String() bodyBytes, marshalErr := json.Marshal(request) if marshalErr != nil { @@ -163,7 +140,7 @@ func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses } req.Header.Add("Content-Type", "application/json") req.Header.Add("User-Agent", h.userAgent) - h.auth(logger, ctx, req) + h.auth(session.Username, logger, req) res, err := h.client.Do(req) if err != nil { diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index d0c974b3c..58c9df51b 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -43,17 +43,17 @@ type WellKnownResponse struct { } type Mailbox struct { - Id string - Name string - ParentId string - Role string - SortOrder int - IsSubscribed bool - TotalEmails int - UnreadEmails int - TotalThreads int - UnreadThreads int - MyRights map[string]bool + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + ParentId string `json:"parentId,omitempty"` + Role string `json:"role,omitempty"` + SortOrder int `json:"sortOrder,omitempty"` + IsSubscribed bool `json:"isSubscribed,omitempty"` + TotalEmails int `json:"totalEmails,omitempty"` + UnreadEmails int `json:"unreadEmails,omitempty"` + TotalThreads int `json:"totalThreads,omitempty"` + UnreadThreads int `json:"unreadThreads,omitempty"` + MyRights map[string]bool `json:"myRights,omitempty"` } type MailboxGetCommand struct { @@ -61,7 +61,40 @@ type MailboxGetCommand struct { Ids []string `json:"ids,omitempty"` } -type Filter struct { +type MailboxGetRefCommand struct { + AccountId string `json:"accountId"` + IdRef *Ref `json:"#ids,omitempty"` +} + +type MailboxFilterCondition struct { + ParentId string `json:"parentId,omitempty"` + Name string `json:"name,omitempty"` + Role string `json:"role,omitempty"` + HasAnyRole *bool `json:"hasAnyRole,omitempty"` + IsSubscribed *bool `json:"isSubscribed,omitempty"` +} + +type MailboxFilterOperator struct { + Operator string `json:"operator"` + Conditions []MailboxFilterCondition `json:"conditions"` +} + +type MailboxComparator struct { + Property string `json:"property"` + IsAscending bool `json:"isAscending,omitempty"` + Limit int `json:"limit,omitzero"` + CalculateTotal bool `json:"calculateTotal,omitempty"` +} + +type SimpleMailboxQueryCommand struct { + AccountId string `json:"accountId"` + Filter MailboxFilterCondition `json:"filter,omitempty"` + Sort []MailboxComparator `json:"sort,omitempty"` + SortAsTree bool `json:"sortAsTree,omitempty"` + FilterAsTree bool `json:"filterAsTree,omitempty"` +} + +type MessageFilter struct { InMailbox string `json:"inMailbox,omitempty"` InMailboxOtherThan []string `json:"inMailboxOtherThan,omitempty"` Before time.Time `json:"before,omitzero"` // omitzero requires Go 1.24 @@ -85,13 +118,13 @@ type Sort struct { } type EmailQueryCommand struct { - AccountId string `json:"accountId"` - Filter *Filter `json:"filter,omitempty"` - Sort []Sort `json:"sort,omitempty"` - CollapseThreads bool `json:"collapseThreads,omitempty"` - Position int `json:"position,omitempty"` - Limit int `json:"limit,omitempty"` - CalculateTotal bool `json:"calculateTotal,omitempty"` + AccountId string `json:"accountId"` + Filter *MessageFilter `json:"filter,omitempty"` + Sort []Sort `json:"sort,omitempty"` + CollapseThreads bool `json:"collapseThreads,omitempty"` + Position int `json:"position,omitempty"` + Limit int `json:"limit,omitempty"` + CalculateTotal bool `json:"calculateTotal,omitempty"` } type Ref struct { @@ -100,7 +133,7 @@ type Ref struct { ResultOf string `json:"resultOf,omitempty"` } -type EmailGetCommand struct { +type EmailGetRefCommand struct { AccountId string `json:"accountId"` FetchAllBodyValues bool `json:"fetchAllBodyValues,omitempty"` MaxBodyValueBytes int `json:"maxBodyValueBytes,omitempty"` @@ -108,49 +141,49 @@ type EmailGetCommand struct { } type EmailAddress struct { - Name string - Email string + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` } type EmailBodyRef struct { - PartId string - BlobId string - Size int - Name string - Type string - Charset string - Disposition string - Cid string - Language string - Location string + PartId string `json:"partId,omitempty"` + BlobId string `json:"blobId,omitempty"` + Size int `json:"size,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Charset string `json:"charset,omitempty"` + Disposition string `json:"disposition,omitempty"` + Cid string `json:"cid,omitempty"` + Language string `json:"language,omitempty"` + Location string `json:"location,omitempty"` } type EmailBody struct { - IsEncodingProblem bool - IsTruncated bool - Value string + IsEncodingProblem bool `json:"isEncodingProblem,omitempty"` + IsTruncated bool `json:"isTruncated,omitempty"` + Value string `json:"value,omitempty"` } type Email struct { - Id string - MessageId []string - BlobId string - ThreadId string - Size int - From []EmailAddress - To []EmailAddress - Cc []EmailAddress - Bcc []EmailAddress - ReplyTo []EmailAddress - Subject string - HasAttachments bool - ReceivedAt time.Time - SentAt time.Time - Preview string - BodyValues map[string]EmailBody - TextBody []EmailBodyRef - HtmlBody []EmailBodyRef - Keywords map[string]bool - MailboxIds map[string]bool + Id string `json:"id,omitempty"` + MessageId []string `json:"messageId,omitempty"` + BlobId string `json:"blobId,omitempty"` + ThreadId string `json:"threadId,omitempty"` + Size int `json:"size,omitempty"` + From []EmailAddress `json:"from,omitempty"` + To []EmailAddress `json:"to,omitempty"` + Cc []EmailAddress `json:"cc,omitempty"` + Bcc []EmailAddress `json:"bcc,omitempty"` + ReplyTo []EmailAddress `json:"replyTo,omitempty"` + Subject string `json:"subject,omitempty"` + HasAttachments bool `json:"hasAttachments,omitempty"` + ReceivedAt time.Time `json:"receivedAt,omitempty"` + SentAt time.Time `json:"sentAt,omitempty"` + Preview string `json:"preview,omitempty"` + BodyValues map[string]EmailBody `json:"bodyValues,omitempty"` + TextBody []EmailBodyRef `json:"textBody,omitempty"` + HtmlBody []EmailBodyRef `json:"htmlBody,omitempty"` + Keywords map[string]bool `json:"keywords,omitempty"` + MailboxIds map[string]bool `json:"mailboxIds,omitempty"` } type Command string diff --git a/pkg/jmap/jmap_reva.go b/pkg/jmap/jmap_reva.go deleted file mode 100644 index a2254c193..000000000 --- a/pkg/jmap/jmap_reva.go +++ /dev/null @@ -1,32 +0,0 @@ -package jmap - -import ( - "context" - "fmt" - "net/http" - - "github.com/opencloud-eu/opencloud/pkg/log" - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" -) - -// HttpJmapUsernameProvider implementation that uses Reva's enrichment of the Context -// to retrieve the current username. -type revaContextHttpJmapUsernameProvider struct { -} - -var _ HttpJmapUsernameProvider = revaContextHttpJmapUsernameProvider{} - -func NewRevaContextHttpJmapUsernameProvider() HttpJmapUsernameProvider { - return revaContextHttpJmapUsernameProvider{} -} - -var errUserNotInContext = fmt.Errorf("user not in context") - -func (r revaContextHttpJmapUsernameProvider) GetUsername(req *http.Request, ctx context.Context, logger *log.Logger) (string, error) { - u, ok := revactx.ContextGetUser(ctx) - if !ok { - logger.Error().Msg("could not get user: user not in reva context") - return "", errUserNotInContext - } - return u.GetUsername(), nil -} diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go index d9e2cb438..dd044dd85 100644 --- a/pkg/jmap/jmap_test.go +++ b/pkg/jmap/jmap_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math/rand" + "net/url" "os" "path/filepath" "testing" @@ -95,7 +96,10 @@ func TestRequests(t *testing.T) { ctx := context.Background() client := NewClient(wkClient, apiClient) - session := Session{AccountId: "123", JmapUrl: "test://"} + jmapUrl, err := url.Parse("http://localhost/jmap") + require.NoError(err) + + session := Session{AccountId: "123", Username: "user123", JmapUrl: *jmapUrl} folders, err := client.GetAllMailboxes(&session, ctx, &logger) require.NoError(err) diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index ca48036e8..2134479c1 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -36,8 +36,6 @@ type Config struct { Keycloak Keycloak `yaml:"keycloak"` ServiceAccount ServiceAccount `yaml:"service_account"` - Mail Mail `yaml:"mail"` - Context context.Context `yaml:"-"` Metadata Metadata `yaml:"metadata_config"` @@ -180,17 +178,3 @@ type Store struct { AuthPassword string `yaml:"password" env:"OC_PERSISTENT_STORE_AUTH_PASSWORD;GRAPH_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"` } -type MasterAuth struct { - Username string `yaml:"username" env:"OC_JMAP_MASTER_USERNAME;GROUPWARE_JMAP_MASTER_USERNAME"` - Password string `yaml:"password" env:"OC_JMAP_MASTER_PASSWORD;GROUPWARE_JMAP_MASTER_PASSWORD"` -} - -type Mail struct { - Master MasterAuth `yaml:"master"` - BaseUrl string `yaml:"base_url" env:"GROUPWARE_BASE_URL"` - JmapUrl string `yaml:"jmap_url" env:"GROUPWARE_JMAP_URL"` - Timeout time.Duration `yaml:"timeout" env:"GROUPWARE_JMAP_TIMEOUT"` - SessionCacheTTL time.Duration `yaml:"session_cache_ttl" env:"GROUPWARE_SESSION_CACHE_TTL"` - DefaultEmailLimit int `yaml:"default_email_limit" env:"GROUPWARE_JMAP_DEFAULT_EMAIL_LIMIT"` - MaxBodyValueBytes int `yaml:"max_body_value_bytes" env:"GROUPWARE_JMAP_MAx_BODY_VALUE_BYTES"` -} diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 55d066c27..9b5352f6e 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -135,18 +135,6 @@ func DefaultConfig() *config.Config { Nodes: []string{"127.0.0.1:9233"}, Database: "graph", }, - Mail: config.Mail{ - Master: config.MasterAuth{ - Username: "master", - Password: "admin", - }, - BaseUrl: "https://stalwart.opencloud.test", - JmapUrl: "https://stalwart.opencloud.test/jmap", - Timeout: time.Duration(3 * time.Second), - SessionCacheTTL: time.Duration(1 * time.Hour), - DefaultEmailLimit: 100, - MaxBodyValueBytes: 0, - }, } } diff --git a/services/graph/pkg/service/v0/groupware/groupware.go b/services/graph/pkg/service/v0/groupware/groupware.go deleted file mode 100644 index 2e760e37c..000000000 --- a/services/graph/pkg/service/v0/groupware/groupware.go +++ /dev/null @@ -1,237 +0,0 @@ -package groupware - -import ( - "context" - "crypto/tls" - "fmt" - "io" - "net/http" - "sync/atomic" - "time" - - "github.com/opencloud-eu/opencloud/pkg/jmap" - "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/graph/pkg/config" - "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" - - "github.com/go-chi/render" - - "github.com/jellydator/ttlcache/v3" -) - -const ( - logFolderId = "folder-id" - logQuery = "query" -) - -type Groupware struct { - logger *log.Logger - jmapClient jmap.Client - sessionCache *ttlcache.Cache[string, jmap.Session] - usernameProvider jmap.HttpJmapUsernameProvider // we also need it for ourselves for now - defaultEmailLimit int - maxBodyValueBytes int - io.Closer -} - -var _ io.Closer = Groupware{} - -type ItemBody struct { - Content string `json:"content"` - ContentType string `json:"contentType"` // text|html -} - -type EmailAddress struct { - Address string `json:"address"` - Name string `json:"name"` -} - -type Messages struct { - Context string `json:"@odata.context,omitempty"` - Value []Message `json:"value"` -} - -type Message struct { - Etag string `json:"@odata.etag,omitempty"` - Id string `json:"id,omitempty"` - CreatedDateTime time.Time `json:"createdDateTime,omitzero"` - ReceivedDateTime time.Time `json:"receivedDateTime,omitzero"` - SentDateTime time.Time `json:"sentDateTime,omitzero"` - HasAttachments bool `json:"hasAttachments,omitempty"` - InternetMessageId string `json:"internetMessageId,omitempty"` - Subject string `json:"subject,omitempty"` - BodyPreview string `json:"bodyPreview,omitempty"` - Body *ItemBody `json:"body,omitempty"` - From *EmailAddress `json:"from,omitempty"` - ToRecipients []EmailAddress `json:"toRecipients,omitempty"` - CcRecipients []EmailAddress `json:"ccRecipients,omitempty"` - BccRecipients []EmailAddress `json:"bccRecipients,omitempty"` - ReplyTo []EmailAddress `json:"replyTo,omitempty"` - IsRead bool `json:"isRead,omitempty"` - IsDraft bool `json:"isDraft,omitempty"` - Importance string `json:"importance,omitempty"` - ParentFolderId string `json:"parentFolderId,omitempty"` - Categories []string `json:"categories,omitempty"` - ConversationId string `json:"conversationId,omitempty"` - WebLink string `json:"webLink,omitempty"` - // ConversationIndex string `json:"conversationIndex"` -} - -func NewGroupware(logger *log.Logger, config *config.Config) *Groupware { - baseUrl := config.Mail.BaseUrl - jmapUrl := config.Mail.JmapUrl - masterUsername := config.Mail.Master.Username - masterPassword := config.Mail.Master.Password - defaultEmailLimit := config.Mail.DefaultEmailLimit - maxBodyValueBytes := config.Mail.MaxBodyValueBytes - - tr := http.DefaultTransport.(*http.Transport).Clone() - tr.ResponseHeaderTimeout = time.Duration(config.Mail.Timeout) - tlsConfig := &tls.Config{InsecureSkipVerify: true} - tr.TLSClientConfig = tlsConfig - c := *http.DefaultClient - c.Transport = tr - - jmapUsernameProvider := jmap.NewRevaContextHttpJmapUsernameProvider() - - api := jmap.NewHttpJmapApiClient( - baseUrl, - jmapUrl, - &c, - jmapUsernameProvider, - masterUsername, - masterPassword, - ) - - jmapClient := jmap.NewClient(api, api) - - sessionCache := ttlcache.New( - ttlcache.WithTTL[string, jmap.Session]( - config.Mail.SessionCacheTTL, - ), - ttlcache.WithDisableTouchOnHit[string, jmap.Session](), - ) - go sessionCache.Start() - - return &Groupware{ - logger: logger, - jmapClient: jmapClient, - sessionCache: sessionCache, - usernameProvider: jmapUsernameProvider, - defaultEmailLimit: defaultEmailLimit, - maxBodyValueBytes: maxBodyValueBytes, - } -} - -func (g Groupware) Close() error { - g.sessionCache.Stop() - return nil -} - -func (g Groupware) session(req *http.Request, ctx context.Context, logger *log.Logger) (jmap.Session, bool, error) { - username, err := g.usernameProvider.GetUsername(req, ctx, logger) - if err != nil { - logger.Error().Err(err).Msg("failed to retrieve username") - return jmap.Session{}, false, err - } - - fetchErrRef := atomic.Value{} - item, _ := g.sessionCache.GetOrSetFunc(username, func() jmap.Session { - jmapContext, err := g.jmapClient.FetchSession(username, logger) - if err != nil { - fetchErrRef.Store(err) - logger.Error().Err(err).Str("username", username).Msg("failed to retrieve well-known") - return jmap.Session{} - } - return jmapContext - }) - p := fetchErrRef.Load() - if p != nil { - err = p.(error) - return jmap.Session{}, false, err - } - if item != nil { - return item.Value(), true, nil - } else { - return jmap.Session{}, false, nil - } -} - -func (g Groupware) withSession(w http.ResponseWriter, r *http.Request, handler func(r *http.Request, ctx context.Context, logger log.Logger, session *jmap.Session) (any, error)) { - ctx := r.Context() - logger := g.logger.SubloggerWithRequestID(ctx) - session, ok, err := g.session(r, ctx, &logger) - if err != nil { - logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("failed to determine JMAP session") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) - return - } - if !ok { - // no session = authentication failed - logger.Warn().Err(err).Interface(logQuery, r.URL.Query()).Msg("could not authenticate") - errorcode.AccessDenied.Render(w, r, http.StatusForbidden, "failed to authenticate") - return - } - logger = session.DecorateLogger(logger) - - response, err := handler(r, ctx, logger, &session) - if err != nil { - logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg(err.Error()) - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) - return - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, response) -} - -func (g Groupware) GetIdentity(w http.ResponseWriter, r *http.Request) { - g.withSession(w, r, func(r *http.Request, ctx context.Context, logger log.Logger, session *jmap.Session) (any, error) { - return g.jmapClient.GetIdentity(session, ctx, &logger) - }) -} - -func (g Groupware) GetVacation(w http.ResponseWriter, r *http.Request) { - g.withSession(w, r, func(r *http.Request, ctx context.Context, logger log.Logger, session *jmap.Session) (any, error) { - return g.jmapClient.GetVacationResponse(session, ctx, &logger) - }) -} - -func (g Groupware) GetMessages(w http.ResponseWriter, r *http.Request) { - g.withSession(w, r, func(r *http.Request, ctx context.Context, logger log.Logger, session *jmap.Session) (any, error) { - offset, ok, _ := parseNumericParam(r, "$skip", 0) - if ok { - logger = log.Logger{Logger: logger.With().Int("$skip", offset).Logger()} - } - limit, ok, _ := parseNumericParam(r, "$top", g.defaultEmailLimit) - if ok { - logger = log.Logger{Logger: logger.With().Int("$top", limit).Logger()} - } - - mailboxGetResponse, err := g.jmapClient.GetAllMailboxes(session, ctx, &logger) - if err != nil { - return nil, err - } - - inboxId := pickInbox(mailboxGetResponse.List) - if inboxId == "" { - return nil, fmt.Errorf("failed to find an inbox folder") - } - logger = log.Logger{Logger: logger.With().Str(logFolderId, inboxId).Logger()} - - emails, err := g.jmapClient.GetEmails(session, ctx, &logger, inboxId, offset, limit, true, g.maxBodyValueBytes) - if err != nil { - return nil, err - } - - messages := make([]Message, 0, len(emails.Emails)) - for _, email := range emails.Emails { - message := message(email, emails.State) - messages = append(messages, message) - } - - odataContext := *r.URL - odataContext.Path = fmt.Sprintf("/graph/v1.0/$metadata#users('%s')/mailFolders('%s')/messages()", session.Username, inboxId) - return Messages{Context: odataContext.String(), Value: messages}, nil - }) -} diff --git a/services/graph/pkg/service/v0/groupware/groupware_test.go b/services/graph/pkg/service/v0/groupware/groupware_test.go deleted file mode 100644 index e6ddcc0b5..000000000 --- a/services/graph/pkg/service/v0/groupware/groupware_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package groupware - -import ( - "time" - - g "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/opencloud-eu/opencloud/pkg/jmap" -) - -var _ = g.Describe("Groupware", func() { - - g.Describe("message", func() { - g.It("copies a JMAP Email to a Graph Message correctly", func() { - now := time.Now() - email := jmap.Email{ - Id: "id", - MessageId: []string{"123.456@example.com"}, - BlobId: "918e929e-a296-4078-915a-2c2abc580a8d", - ThreadId: "t", - Size: 12345, - From: []jmap.EmailAddress{ - {Name: "Bobbie Draper", Email: "bobbie@mcrn.mars"}, - }, - To: []jmap.EmailAddress{ - {Name: "Camina Drummer", Email: "camina@opa.org"}, - }, - Subject: "test subject", - HasAttachments: true, - ReceivedAt: now, - Preview: "the preview", - TextBody: []jmap.EmailBodyRef{ - {PartId: "0", Type: "text/plain"}, - }, - BodyValues: map[string]jmap.EmailBody{ - "0": { - IsEncodingProblem: false, - IsTruncated: false, - Value: "the body", - }, - }, - } - - msg := message(email, "aaa") - Expect(msg.Body.ContentType).To(Equal("text/plain")) - Expect(msg.Body.Content).To(Equal("the body")) - Expect(msg.Subject).To(Equal("test subject")) - }) - }) -}) diff --git a/services/graph/pkg/service/v0/groupware/groupware_tools.go b/services/graph/pkg/service/v0/groupware/groupware_tools.go deleted file mode 100644 index 4fa069c7a..000000000 --- a/services/graph/pkg/service/v0/groupware/groupware_tools.go +++ /dev/null @@ -1,176 +0,0 @@ -package groupware - -import ( - "net/http" - "net/url" - "strconv" - "strings" - - "github.com/opencloud-eu/opencloud/pkg/jmap" -) - -func pickInbox(folders []jmap.Mailbox) string { - for _, folder := range folders { - if folder.Role == "inbox" { - return folder.Id - } - } - return "" -} - -func mapContentType(jmap string) string { - switch jmap { - case "text/html": - return "html" - case "text/plain": - return "text" - default: - return jmap - } -} - -func foldBody(email jmap.Email) *ItemBody { - if email.BodyValues != nil { - if len(email.HtmlBody) > 0 { - pick := email.HtmlBody[0] - content, ok := email.BodyValues[pick.PartId] - if ok { - return &ItemBody{Content: content.Value, ContentType: mapContentType(pick.Type)} - } - } - if len(email.TextBody) > 0 { - pick := email.TextBody[0] - content, ok := email.BodyValues[pick.PartId] - if ok { - return &ItemBody{Content: content.Value, ContentType: mapContentType(pick.Type)} - } - } - } - return nil -} - -func firstOf[T any](ary []T) T { - if len(ary) > 0 { - return ary[0] - } - var nothing T - return nothing -} - -func emailAddress(j jmap.EmailAddress) EmailAddress { - return EmailAddress{Address: j.Email, Name: j.Name} -} - -func emailAddresses(j []jmap.EmailAddress) []EmailAddress { - result := make([]EmailAddress, len(j)) - for i := 0; i < len(j); i++ { - result[i] = emailAddress(j[i]) - } - return result -} - -func hasKeyword(j jmap.Email, kw string) bool { - value, ok := j.Keywords[kw] - if ok { - return value - } - return false -} - -func categories(j jmap.Email) []string { - categories := []string{} - for k, v := range j.Keywords { - if v && !strings.HasPrefix(k, jmap.JmapKeywordPrefix) { - categories = append(categories, k) - } - } - return categories -} - -/* -func toEdmBinary(value int) string { - return fmt.Sprintf("%X", value) -} -*/ - -// https://learn.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0 -func message(email jmap.Email, state string) Message { - body := foldBody(email) - importance := "" // omit "normal" as it is expected to be the default - if hasKeyword(email, jmap.JmapKeywordFlagged) { - importance = "high" - } - - mailboxId := "" - for k, v := range email.MailboxIds { - if v { - // TODO how to map JMAP short identifiers (e.g. 'a') to something uniquely addressable for the clients? - // e.g. do we need to include tenant/sharding/cluster information? - mailboxId = k - break - } - } - - // TODO how to map JMAP short identifiers (e.g. 'a') to something uniquely addressable for the clients? - // e.g. do we need to include tenant/sharding/cluster information? - id := email.Id - // for this one too: - messageId := firstOf(email.MessageId) - // as well as this one: - threadId := email.ThreadId - - categories := categories(email) - - var from *EmailAddress = nil - if len(email.From) > 0 { - e := emailAddress(email.From[0]) - from = &e - } - - // TODO how to map JMAP state to an OData Etag? - etag := state - - weblink, err := url.JoinPath("/groupware/mail", id) - if err != nil { - weblink = "" - } - - return Message{ - Etag: etag, - Id: id, - Subject: email.Subject, - CreatedDateTime: email.ReceivedAt, - ReceivedDateTime: email.ReceivedAt, - SentDateTime: email.SentAt, - HasAttachments: email.HasAttachments, - InternetMessageId: messageId, - BodyPreview: email.Preview, - Body: body, - From: from, - ToRecipients: emailAddresses(email.To), - CcRecipients: emailAddresses(email.Cc), - BccRecipients: emailAddresses(email.Bcc), - ReplyTo: emailAddresses(email.ReplyTo), - IsRead: hasKeyword(email, jmap.JmapKeywordSeen), - IsDraft: hasKeyword(email, jmap.JmapKeywordDraft), - Importance: importance, - ParentFolderId: mailboxId, - Categories: categories, - ConversationId: threadId, - WebLink: weblink, - // ConversationIndex: toEdmBinary(email.ThreadIndex), - } // TODO more email fields -} - -func parseNumericParam(r *http.Request, param string, defaultValue int) (int, bool, error) { - str := r.URL.Query().Get(param) - if str == "" { - return defaultValue, false, nil - } - - value, err := strconv.ParseInt(str, 10, 0) - if err != nil { - return defaultValue, false, nil - } - return int(value), true, nil -} diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 6390b1bf6..f130de6dc 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -34,7 +34,6 @@ import ( settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" "github.com/opencloud-eu/opencloud/services/graph/pkg/identity" graphm "github.com/opencloud-eu/opencloud/services/graph/pkg/middleware" - "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0/groupware" "github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole" ) @@ -203,8 +202,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx natskv: options.NatsKeyValue, } - gw := groupware.NewGroupware(&options.Logger, options.Config) - if err := setIdentityBackends(options, &svc); err != nil { return svc, err } @@ -321,9 +318,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx r.Patch("/", usersUserProfilePhotoApi.UpsertProfilePhoto(GetUserIDFromCTX)) r.Delete("/", usersUserProfilePhotoApi.DeleteProfilePhoto(GetUserIDFromCTX)) }) - r.Get("/messages", gw.GetMessages) - r.Get("/identity", gw.GetIdentity) - r.Get("/vacation", gw.GetVacation) }) r.Route("/users", func(r chi.Router) { r.Get("/", svc.GetUsers) diff --git a/services/groupware/pkg/config/config.go b/services/groupware/pkg/config/config.go index bfcec4a64..149bbd96c 100644 --- a/services/groupware/pkg/config/config.go +++ b/services/groupware/pkg/config/config.go @@ -21,17 +21,23 @@ type Config struct { Mail Mail `yaml:"mail"` + TokenManager *TokenManager `yaml:"token_manager"` + Context context.Context `yaml:"-"` } -type MasterAuth struct { - Username string `yaml:"username" env:"OC_JMAP_MASTER_USERNAME;GROUPWARE_JMAP_MASTER_USERNAME"` - Password string `yaml:"password" env:"OC_JMAP_MASTER_PASSWORD;GROUPWARE_JMAP_MASTER_PASSWORD"` +type MailMasterAuth struct { + Username string `yaml:"username" env:"GROUPWARE_JMAP_MASTER_USERNAME"` + Password string `yaml:"password" env:"GROUPWARE_JMAP_MASTER_PASSWORD"` } type Mail struct { - Master MasterAuth `yaml:"master"` - BaseUrl string `yaml:"base_url" env:"OC_JMAP_BASE_URL;GROUPWARE_BASE_URL"` - JmapUrl string `yaml:"jmap_url" env:"OC_JMAP_JMAP_URL;GROUPWARE_JMAP_URL"` - Timeout time.Duration `yaml:"timeout" env:"OC_JMAP_TIMEOUT"` + Master MailMasterAuth `yaml:"master"` + BaseUrl string `yaml:"base_url" env:"GROUPWARE_JMAP_BASE_URL"` + Timeout time.Duration `yaml:"timeout" env:"GROUPWARE_JMAP_TIMEOUT"` + DefaultEmailLimit int `yaml:"default_email_limit" env:"GROUPWARE_DEFAULT_EMAIL_LIMIT"` + MaxBodyValueBytes int `yaml:"max_body_value_bytes" env:"GROUPWARE_MAX_BODY_VALUE_BYTES"` + ResponseHeaderTimeout time.Duration `yaml:"response_header_timeout" env:"GROUPWARE_RESPONSE_HEADER_TIMEOUT"` + SessionCacheTtl time.Duration `yaml:"session_cache_ttl" env:"GROUPWARE_SESSION_CACHE_TTL"` + SessionFailureCacheTtl time.Duration `yaml:"session_failure_cache_ttl" env:"GROUPWARE_SESSION_FAILURE_CACHE_TTL"` } diff --git a/services/groupware/pkg/config/defaults/defaultconfig.go b/services/groupware/pkg/config/defaults/defaultconfig.go index d227ba36b..1d63e4d22 100644 --- a/services/groupware/pkg/config/defaults/defaultconfig.go +++ b/services/groupware/pkg/config/defaults/defaultconfig.go @@ -2,6 +2,7 @@ package defaults import ( "strings" + "time" "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" ) @@ -18,18 +19,23 @@ func FullDefaultConfig() *config.Config { func DefaultConfig() *config.Config { return &config.Config{ Debug: config.Debug{ - Addr: "127.0.0.1:9202", + Addr: "127.0.0.1:9292", Token: "", Pprof: false, Zpages: false, }, Mail: config.Mail{ - Master: config.MasterAuth{ - Username: "masteradmin", + Master: config.MailMasterAuth{ + Username: "master", Password: "admin", }, - BaseUrl: "https://stalwart.opencloud.test", - JmapUrl: "https://stalwart.opencloud.test/jmap", + BaseUrl: "https://stalwart.opencloud.test", + Timeout: 30 * time.Second, + DefaultEmailLimit: -1, + MaxBodyValueBytes: -1, + ResponseHeaderTimeout: 10 * time.Second, + SessionCacheTtl: 30 * time.Second, + SessionFailureCacheTtl: 15 * time.Second, }, HTTP: config.HTTP{ Addr: "127.0.0.1:9276", @@ -72,13 +78,16 @@ func EnsureDefaults(cfg *config.Config) { } else if cfg.Tracing == nil { cfg.Tracing = &config.Tracing{} } - + if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil { + cfg.TokenManager = &config.TokenManager{ + JWTSecret: cfg.Commons.TokenManager.JWTSecret, + } + } else if cfg.TokenManager == nil { + cfg.TokenManager = &config.TokenManager{} + } if cfg.Commons != nil { cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS } - - // TODO p.bleser add Mail here - } // Sanitize sanitized the configuration diff --git a/services/groupware/pkg/config/parser/parse.go b/services/groupware/pkg/config/parser/parse.go index e02236f56..61f1e0dd4 100644 --- a/services/groupware/pkg/config/parser/parse.go +++ b/services/groupware/pkg/config/parser/parse.go @@ -4,6 +4,7 @@ import ( "errors" occfg "github.com/opencloud-eu/opencloud/pkg/config" + "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" "github.com/opencloud-eu/opencloud/services/groupware/pkg/config/defaults" @@ -34,6 +35,10 @@ func ParseConfig(cfg *config.Config) error { } // Validate can validate the configuration -func Validate(_ *config.Config) error { +func Validate(cfg *config.Config) error { + if cfg.TokenManager.JWTSecret == "" { + return shared.MissingJWTTokenError(cfg.Service.Name) + } + return nil } diff --git a/services/groupware/pkg/config/reva.go b/services/groupware/pkg/config/reva.go new file mode 100644 index 000000000..a357bdf54 --- /dev/null +++ b/services/groupware/pkg/config/reva.go @@ -0,0 +1,6 @@ +package config + +// TokenManager is the config for using the reva token manager +type TokenManager struct { + JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;GROUPWARE_JWT_SECRET" desc:"The secret to mint and validate jwt tokens."` +} diff --git a/services/groupware/pkg/groupware/groupware.go b/services/groupware/pkg/groupware/groupware.go new file mode 100644 index 000000000..cdf5b5348 --- /dev/null +++ b/services/groupware/pkg/groupware/groupware.go @@ -0,0 +1,162 @@ +package groupware + +import ( + "context" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + + "github.com/opencloud-eu/opencloud/pkg/jmap" + "github.com/opencloud-eu/opencloud/pkg/log" +) + +func (g Groupware) Route(r chi.Router) { + r.Get("/", g.Index) + r.Get("/ping", g.Ping) + r.Get("/mailboxes", g.GetMailboxes) // ?name=&role=&subcribed= + r.Get("/mailbox/{id}", g.GetMailboxById) + r.Get("/{mailbox}/messages", g.GetMessages) + r.Get("/identity", g.GetIdentity) + r.Get("/vacation", g.GetVacation) +} + +type IndexResponse struct { + AccountId string +} + +func (IndexResponse) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +func (g Groupware) Ping(w http.ResponseWriter, r *http.Request) { + g.logger.Info().Msg("groupware pinged") + w.WriteHeader(http.StatusNoContent) +} + +func (g Groupware) Index(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + logger := g.logger.SubloggerWithRequestID(ctx) + + session, ok, err := g.session(r, ctx, &logger) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if ok { + //logger = session.DecorateLogger(logger) + _ = render.Render(w, r, IndexResponse{AccountId: session.AccountId}) + } else { + w.WriteHeader(http.StatusInternalServerError) + return + } + +} + +func (g Groupware) GetIdentity(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) { + res, err := g.jmap.GetIdentity(session, ctx, logger) + return res, res.State, err + }) +} + +func (g Groupware) GetVacation(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) { + res, err := g.jmap.GetVacationResponse(session, ctx, logger) + return res, res.State, err + }) +} + +func (g Groupware) GetMailboxById(w http.ResponseWriter, r *http.Request) { + mailboxId := chi.URLParam(r, "mailbox") + if mailboxId == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) { + res, err := g.jmap.GetMailbox(session, ctx, logger, []string{mailboxId}) + if err != nil { + return res, "", err + } + + if len(res.List) == 1 { + return res.List[0], res.State, err + } else { + return nil, res.State, err + } + }) +} + +func (g Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + var filter jmap.MailboxFilterCondition + + hasCriteria := false + name := q.Get("name") + if name != "" { + filter.Name = name + hasCriteria = true + } + role := q.Get("role") + if role != "" { + filter.Role = role + hasCriteria = true + } + subscribed := q.Get("subscribed") + if subscribed != "" { + b, err := strconv.ParseBool(subscribed) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + filter.IsSubscribed = &b + hasCriteria = true + } + + if hasCriteria { + g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) { + mailboxes, err := g.jmap.SearchMailboxes(session, ctx, logger, filter) + if err != nil { + return nil, "", err + } + return mailboxes.Mailboxes, mailboxes.State, nil + }) + } else { + g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) { + mailboxes, err := g.jmap.GetAllMailboxes(session, ctx, logger) + if err != nil { + return nil, "", err + } + return mailboxes.List, mailboxes.State, nil + }) + } +} + +func (g Groupware) GetMessages(w http.ResponseWriter, r *http.Request) { + mailboxId := chi.URLParam(r, "mailbox") + g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) { + page, ok, _ := ParseNumericParam(r, "page", -1) + if ok { + logger = &log.Logger{Logger: logger.With().Int("page", page).Logger()} + } + size, ok, _ := ParseNumericParam(r, "size", -1) + if ok { + logger = &log.Logger{Logger: logger.With().Int("size", size).Logger()} + } + + offset := page * size + limit := size + if limit < 0 { + limit = g.defaultEmailLimit + } + + emails, err := g.jmap.GetEmails(session, ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes) + if err != nil { + return nil, "", err + } + + return emails, emails.State, nil + }) +} diff --git a/services/groupware/pkg/groupware/groupware_lowlevel.go b/services/groupware/pkg/groupware/groupware_lowlevel.go new file mode 100644 index 000000000..f5a4e6aad --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_lowlevel.go @@ -0,0 +1,282 @@ +package groupware + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + + "github.com/jellydator/ttlcache/v3" + + "github.com/opencloud-eu/opencloud/pkg/jmap" + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" +) + +const ( + logFolderId = "folder-id" + logQuery = "query" +) + +type cachedSession interface { + Success() bool + Get() jmap.Session + Error() error +} + +type succeededSession struct { + session jmap.Session +} + +func (s succeededSession) Success() bool { + return true +} +func (s succeededSession) Get() jmap.Session { + return s.session +} +func (s succeededSession) Error() error { + return nil +} + +var _ cachedSession = succeededSession{} + +type failedSession struct { + err error +} + +func (s failedSession) Success() bool { + return false +} +func (s failedSession) Get() jmap.Session { + panic("this should never be called") +} +func (s failedSession) Error() error { + return s.err +} + +var _ cachedSession = failedSession{} + +type sessionCacheLoader struct { + logger *log.Logger + jmapClient jmap.Client + errorTtl time.Duration +} + +func (l *sessionCacheLoader) Load(c *ttlcache.Cache[string, cachedSession], username string) *ttlcache.Item[string, cachedSession] { + session, err := l.jmapClient.FetchSession(username, l.logger) + if err != nil { + l.logger.Warn().Str("username", username).Err(err).Msgf("failed to create session for '%v'", username) + return c.Set(username, failedSession{err: err}, l.errorTtl) + } else { + l.logger.Debug().Str("username", username).Msgf("successfully created session for '%v'", username) + return c.Set(username, succeededSession{session: session}, 0) // 0 = use the TTL configured on the Cache + } +} + +var _ ttlcache.Loader[string, cachedSession] = &sessionCacheLoader{} + +type Groupware struct { + mux *chi.Mux + logger *log.Logger + defaultEmailLimit int + maxBodyValueBytes int + sessionCache *ttlcache.Cache[string, cachedSession] + jmap jmap.Client + usernameProvider UsernameProvider +} + +type GroupwareInitializationError struct { + Message string + Err error +} + +func (e GroupwareInitializationError) Error() string { + if e.Message != "" { + return fmt.Sprintf("failed to create Groupware: %s: %v", e.Message, e.Err.Error()) + } else { + return fmt.Sprintf("failed to create Groupware: %v", e.Err.Error()) + } +} +func (e GroupwareInitializationError) Unwrap() error { + return e.Err +} + +func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux) (*Groupware, error) { + baseUrl, err := url.Parse(config.Mail.BaseUrl) + if err != nil { + logger.Error().Err(err).Msgf("failed to parse configured Mail.Baseurl '%v'", config.Mail.BaseUrl) + return nil, GroupwareInitializationError{Message: fmt.Sprintf("failed to parse configured Mail.BaseUrl '%s'", config.Mail.BaseUrl), Err: err} + } + + masterUsername := config.Mail.Master.Username + if masterUsername == "" { + logger.Error().Msg("failed to parse empty Mail.Master.Username") + return nil, GroupwareInitializationError{Message: "Mail.Master.Username is empty"} + } + masterPassword := config.Mail.Master.Password + if masterPassword == "" { + logger.Error().Msg("failed to parse empty Mail.Master.Password") + return nil, GroupwareInitializationError{Message: "Mail.Master.Password is empty"} + } + + defaultEmailLimit := config.Mail.DefaultEmailLimit + if defaultEmailLimit < 0 { + defaultEmailLimit = 0 + } + maxBodyValueBytes := config.Mail.MaxBodyValueBytes + if maxBodyValueBytes < 0 { + maxBodyValueBytes = 0 + } + + responseHeaderTimeout := config.Mail.ResponseHeaderTimeout + if responseHeaderTimeout < 0 { + responseHeaderTimeout = 0 + } + sessionCacheTtl := config.Mail.SessionCacheTtl + if sessionCacheTtl < 0 { + sessionCacheTtl = 0 + } + sessionFailureCacheTtl := config.Mail.SessionFailureCacheTtl + if sessionFailureCacheTtl < 0 { + sessionFailureCacheTtl = 0 + } + + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.ResponseHeaderTimeout = responseHeaderTimeout + tlsConfig := &tls.Config{InsecureSkipVerify: true} + tr.TLSClientConfig = tlsConfig + c := *http.DefaultClient + c.Transport = tr + + usernameProvider := NewRevaContextUsernameProvider() + + api := jmap.NewHttpJmapApiClient( + *baseUrl, + &c, + masterUsername, + masterPassword, + ) + + jmapClient := jmap.NewClient(api, api) + + var sessionCache *ttlcache.Cache[string, cachedSession] + { + sessionLoader := &sessionCacheLoader{ + logger: logger, + jmapClient: jmapClient, + errorTtl: sessionFailureCacheTtl, + } + + sessionCache = ttlcache.New( + ttlcache.WithTTL[string, cachedSession]( + sessionCacheTtl, + ), + ttlcache.WithDisableTouchOnHit[string, cachedSession](), + ttlcache.WithLoader(sessionLoader), + ) + go sessionCache.Start() + } + + return &Groupware{ + mux: mux, + logger: logger, + sessionCache: sessionCache, + usernameProvider: usernameProvider, + jmap: jmapClient, + defaultEmailLimit: defaultEmailLimit, + maxBodyValueBytes: maxBodyValueBytes, + }, nil +} + +func (g Groupware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + g.mux.ServeHTTP(w, r) +} + +func (g Groupware) session(req *http.Request, ctx context.Context, logger *log.Logger) (jmap.Session, bool, error) { + username, ok, err := g.usernameProvider.GetUsername(req, ctx, logger) + if err != nil { + logger.Error().Err(err).Msg("failed to retrieve username") + return jmap.Session{}, false, err + } + if !ok { + logger.Debug().Msg("unauthenticated API access attempt") + return jmap.Session{}, false, nil + } + + item := g.sessionCache.Get(username) + if item != nil { + value := item.Value() + if value != nil { + if value.Success() { + return value.Get(), true, nil + } else { + return jmap.Session{}, false, value.Error() + } + } + } + return jmap.Session{}, false, nil +} + +func (g Groupware) respond(w http.ResponseWriter, r *http.Request, + handler func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error)) { + ctx := r.Context() + logger := g.logger.SubloggerWithRequestID(ctx) + session, ok, err := g.session(r, ctx, &logger) + if err != nil { + logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("failed to determine JMAP session") + w.WriteHeader(http.StatusInternalServerError) + return + } + if !ok { + // no session = authentication failed + logger.Warn().Err(err).Interface(logQuery, r.URL.Query()).Msg("could not authenticate") + w.WriteHeader(http.StatusForbidden) + return + } + logger = session.DecorateLogger(logger) + + response, state, err := handler(r, ctx, &logger, &session) + if err != nil { + logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if state != "" { + w.Header().Add("ETag", state) + } + if response == nil { + w.WriteHeader(http.StatusNotFound) + } else { + render.Status(r, http.StatusOK) + render.JSON(w, r, response) + } +} + +func (g Groupware) withSession(w http.ResponseWriter, r *http.Request, + handler func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error)) (any, string, error) { + ctx := r.Context() + logger := g.logger.SubloggerWithRequestID(ctx) + session, ok, err := g.session(r, ctx, &logger) + if err != nil { + logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("failed to determine JMAP session") + return nil, "", err + } + if !ok { + // no session = authentication failed + logger.Warn().Err(err).Interface(logQuery, r.URL.Query()).Msg("could not authenticate") + return nil, "", err + } + logger = session.DecorateLogger(logger) + + response, state, err := handler(r, ctx, &logger, &session) + if err != nil { + logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg(err.Error()) + } + return response, state, err +} diff --git a/services/groupware/pkg/groupware/groupware_reva.go b/services/groupware/pkg/groupware/groupware_reva.go new file mode 100644 index 000000000..4572a603b --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_reva.go @@ -0,0 +1,36 @@ +package groupware + +import ( + "context" + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/log" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" +) + +// UsernameProvider implementation that uses Reva's enrichment of the Context +// to retrieve the current username. +type revaContextUsernameProvider struct { +} + +type UsernameProvider interface { + // Provide the username for JMAP operations. + GetUsername(req *http.Request, ctx context.Context, logger *log.Logger) (string, bool, error) +} + +var _ UsernameProvider = revaContextUsernameProvider{} + +func NewRevaContextUsernameProvider() UsernameProvider { + return revaContextUsernameProvider{} +} + +// var errUserNotInContext = fmt.Errorf("user not in context") + +func (r revaContextUsernameProvider) GetUsername(req *http.Request, ctx context.Context, logger *log.Logger) (string, bool, error) { + u, ok := revactx.ContextGetUser(ctx) + if !ok { + logger.Error().Ctx(ctx).Msgf("could not get user: user not in reva context: %v", ctx) + return "", false, nil + } + return u.GetUsername(), true, nil +} diff --git a/services/groupware/pkg/groupware/groupware_tools.go b/services/groupware/pkg/groupware/groupware_tools.go new file mode 100644 index 000000000..6400742d9 --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_tools.go @@ -0,0 +1,30 @@ +package groupware + +import ( + "net/http" + "strconv" + + "github.com/opencloud-eu/opencloud/pkg/jmap" +) + +func ParseNumericParam(r *http.Request, param string, defaultValue int) (int, bool, error) { + str := r.URL.Query().Get(param) + if str == "" { + return defaultValue, false, nil + } + + value, err := strconv.ParseInt(str, 10, 0) + if err != nil { + return defaultValue, false, nil + } + return int(value), true, nil +} + +func PickInbox(folders []jmap.Mailbox) string { + for _, folder := range folders { + if folder.Role == "inbox" { + return folder.Id + } + } + return "" +} diff --git a/services/groupware/pkg/middleware/auth.go b/services/groupware/pkg/middleware/auth.go new file mode 100644 index 000000000..6c8b76fb8 --- /dev/null +++ b/services/groupware/pkg/middleware/auth.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "net/http" + + gmmetadata "go-micro.dev/v4/metadata" + "google.golang.org/grpc/metadata" + + "github.com/opencloud-eu/opencloud/pkg/account" + "github.com/opencloud-eu/opencloud/pkg/log" + opkgm "github.com/opencloud-eu/opencloud/pkg/middleware" + "github.com/opencloud-eu/reva/v2/pkg/auth/scope" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/token/manager/jwt" +) + +// authOptions initializes the available default options. +func authOptions(opts ...account.Option) account.Options { + opt := account.Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +func Auth(opts ...account.Option) func(http.Handler) http.Handler { + opt := authOptions(opts...) + tokenManager, err := jwt.New(map[string]any{ + "secret": opt.JWTSecret, + "expires": int64(24 * 60 * 60), + }) + if err != nil { + opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager") + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + t := r.Header.Get(revactx.TokenHeader) + if t == "" { + opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Msgf("missing access token in header %v", revactx.TokenHeader) + w.WriteHeader(http.StatusUnauthorized) // missing access token + return + } + + u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t) + if err != nil { + opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msgf("invalid access token in header %v", revactx.TokenHeader) + w.WriteHeader(http.StatusUnauthorized) // invalid token + return + } + if ok, err := scope.VerifyScope(ctx, tokenScope, r); err != nil || !ok { + opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msg("verifying scope failed") + w.WriteHeader(http.StatusUnauthorized) // invalid scope + return + } + + ctx = revactx.ContextSetToken(ctx, t) + ctx = revactx.ContextSetUser(ctx, u) + ctx = gmmetadata.Set(ctx, opkgm.AccountID, u.GetId().GetOpaqueId()) + if m := u.GetOpaque().GetMap(); m != nil { + if roles, ok := m["roles"]; ok { + ctx = gmmetadata.Set(ctx, opkgm.RoleIDs, string(roles.GetValue())) + } + } + ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, t) + + initiatorID := r.Header.Get(revactx.InitiatorHeader) + if initiatorID != "" { + ctx = revactx.ContextSetInitiator(ctx, initiatorID) + ctx = metadata.AppendToOutgoingContext(ctx, revactx.InitiatorHeader, initiatorID) + } + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/services/groupware/pkg/server/http/server.go b/services/groupware/pkg/server/http/server.go index 67c907391..5c5320ee7 100644 --- a/services/groupware/pkg/server/http/server.go +++ b/services/groupware/pkg/server/http/server.go @@ -4,10 +4,12 @@ import ( "fmt" "github.com/go-chi/chi/v5/middleware" + "github.com/opencloud-eu/opencloud/pkg/account" "github.com/opencloud-eu/opencloud/pkg/cors" opencloudmiddleware "github.com/opencloud-eu/opencloud/pkg/middleware" "github.com/opencloud-eu/opencloud/pkg/service/http" "github.com/opencloud-eu/opencloud/pkg/version" + groupwaremiddleware "github.com/opencloud-eu/opencloud/services/groupware/pkg/middleware" svc "github.com/opencloud-eu/opencloud/services/groupware/pkg/service/http/v0" "go-micro.dev/v4" ) @@ -33,7 +35,7 @@ func Server(opts ...Option) (http.Service, error) { return http.Service{}, fmt.Errorf("could not initialize http service: %w", err) } - handle := svc.NewService( + handle, err := svc.NewService( svc.Logger(options.Logger), svc.Config(options.Config), svc.Middleware( @@ -51,8 +53,15 @@ func Server(opts ...Option) (http.Service, error) { version.GetString(), ), opencloudmiddleware.Logger(options.Logger), + groupwaremiddleware.Auth( + account.Logger(options.Logger), + account.JWTSecret(options.Config.TokenManager.JWTSecret), + ), ), ) + if err != nil { + return http.Service{}, err + } { handle = svc.NewInstrument(handle, options.Metrics) diff --git a/services/groupware/pkg/service/http/v0/service.go b/services/groupware/pkg/service/http/v0/service.go index 2ce683503..d0f4f2534 100644 --- a/services/groupware/pkg/service/http/v0/service.go +++ b/services/groupware/pkg/service/http/v0/service.go @@ -1,18 +1,13 @@ package svc import ( - "crypto/tls" "net/http" - "time" "github.com/go-chi/chi/v5" - "github.com/go-chi/render" "github.com/riandyrn/otelchi" - "github.com/opencloud-eu/opencloud/pkg/jmap" - "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/tracing" - "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/groupware" ) // Service defines the service handlers. @@ -21,7 +16,7 @@ type Service interface { } // NewService returns a service implementation for Service. -func NewService(opts ...Option) Service { +func NewService(opts ...Option) (Service, error) { options := newOptions(opts...) m := chi.NewMux() @@ -36,89 +31,17 @@ func NewService(opts ...Option) Service { ), ) - svc := NewGroupware(options.Config, &options.Logger, m) + gw, err := groupware.NewGroupware(options.Config, &options.Logger, m) + if err != nil { + return nil, err + } - m.Route(options.Config.HTTP.Root, func(r chi.Router) { - r.Get("/", svc.WellDefined) - r.Get("/ping", svc.Ping) - }) + m.Route(options.Config.HTTP.Root, gw.Route) _ = chi.Walk(m, func(method string, route string, _ http.Handler, middlewares ...func(http.Handler) http.Handler) error { options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") return nil }) - return svc -} - -type Groupware struct { - jmapClient jmap.Client - usernameProvider jmap.HttpJmapUsernameProvider - config *config.Config - logger *log.Logger - mux *chi.Mux -} - -func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux) *Groupware { - usernameProvider := jmap.NewRevaContextHttpJmapUsernameProvider() - httpApiClient := httpApiClient(config, usernameProvider) - jmapClient := jmap.NewClient(httpApiClient, httpApiClient) - return &Groupware{ - jmapClient: jmapClient, - usernameProvider: usernameProvider, - config: config, - mux: mux, - logger: logger, - } -} - -func (g Groupware) ServeHTTP(w http.ResponseWriter, r *http.Request) { - g.mux.ServeHTTP(w, r) -} - -type IndexResponse struct { - AccountId string -} - -func (IndexResponse) Render(w http.ResponseWriter, r *http.Request) error { - return nil -} - -func (g Groupware) Ping(w http.ResponseWriter, r *http.Request) { - g.logger.Info().Msg("groupware pinged") - w.WriteHeader(http.StatusNoContent) -} - -func httpApiClient(config *config.Config, usernameProvider jmap.HttpJmapUsernameProvider) *jmap.HttpJmapApiClient { - tr := http.DefaultTransport.(*http.Transport).Clone() - tr.ResponseHeaderTimeout = time.Duration(10) * time.Second - tlsConfig := &tls.Config{InsecureSkipVerify: true} - tr.TLSClientConfig = tlsConfig - c := *http.DefaultClient - c.Transport = tr - - api := jmap.NewHttpJmapApiClient( - config.Mail.BaseUrl, - config.Mail.JmapUrl, - &c, - usernameProvider, - config.Mail.Master.Username, - config.Mail.Master.Password, - ) - return api -} -func (g Groupware) WellDefined(w http.ResponseWriter, r *http.Request) { - logger := g.logger.SubloggerWithRequestID(r.Context()) - username, err := g.usernameProvider.GetUsername(r, r.Context(), &logger) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - jmapContext, err := g.jmapClient.FetchSession(username, &logger) - if err != nil { - return - } - - _ = render.Render(w, r, IndexResponse{AccountId: jmapContext.AccountId}) + return gw, nil } diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index b114949a2..493d374e4 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -292,9 +292,8 @@ func DefaultPolicies() []config.Policy { SkipXAccessToken: true, }, { - Endpoint: "/groupware", - Service: "eu.opencloud.web.groupware", - Unprotected: true, + Endpoint: "/groupware", + Service: "eu.opencloud.web.groupware", }, { Endpoint: "/auth",