mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-07 04:41:31 -05:00
462 lines
14 KiB
Go
462 lines
14 KiB
Go
package groupware
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
|
"github.com/opencloud-eu/opencloud/pkg/log"
|
|
"github.com/opencloud-eu/opencloud/pkg/structs"
|
|
|
|
"github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics"
|
|
groupwaremiddleware "github.com/opencloud-eu/opencloud/services/groupware/pkg/middleware"
|
|
)
|
|
|
|
const (
|
|
// TODO remove this once Stalwart has actual support for Tasks, Calendars, Contacts and we don't need to mock it any more
|
|
IgnoreSessionCapabilityChecks = true
|
|
)
|
|
|
|
// using a wrapper class for requests, to group multiple parameters, really to avoid crowding the
|
|
// API of handlers but also to make it easier to expand it in the future without having to modify
|
|
// the parameter list of every single handler function
|
|
type Request struct {
|
|
g *Groupware
|
|
user user
|
|
r *http.Request
|
|
ctx context.Context
|
|
logger *log.Logger
|
|
session *jmap.Session
|
|
}
|
|
|
|
func isDefaultAccountid(accountId string) bool {
|
|
return slices.Contains(defaultAccountIds, accountId)
|
|
}
|
|
|
|
func (r Request) push(typ string, event any) {
|
|
r.g.push(r.user, typ, event)
|
|
}
|
|
|
|
func (r Request) GetUser() user {
|
|
return r.user
|
|
}
|
|
|
|
func (r Request) GetRequestId() string {
|
|
return chimiddleware.GetReqID(r.ctx)
|
|
}
|
|
|
|
func (r Request) GetTraceId() string {
|
|
return groupwaremiddleware.GetTraceID(r.ctx)
|
|
}
|
|
|
|
var (
|
|
errNoPrimaryAccountFallback = errors.New("no primary account fallback")
|
|
errNoPrimaryAccountForMail = errors.New("no primary account for mail")
|
|
errNoPrimaryAccountForBlob = errors.New("no primary account for blob")
|
|
errNoPrimaryAccountForVacationResponse = errors.New("no primary account for vacation response")
|
|
errNoPrimaryAccountForSubmission = errors.New("no primary account for submission")
|
|
errNoPrimaryAccountForTask = errors.New("no primary account for task")
|
|
errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
|
|
errNoPrimaryAccountForContact = errors.New("no primary account for contact")
|
|
errNoPrimaryAccountForQuota = errors.New("no primary account for quota")
|
|
// errNoPrimaryAccountForSieve = errors.New("no primary account for sieve")
|
|
// errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket")
|
|
)
|
|
|
|
func (r Request) AllAccountIds() []string {
|
|
// TODO potentially filter on "subscribed" accounts?
|
|
return structs.Uniq(structs.Keys(r.session.Accounts))
|
|
}
|
|
|
|
func (r Request) GetAccountIdWithoutFallback() (string, *Error) {
|
|
accountId := chi.URLParam(r.r, UriParamAccountId)
|
|
if accountId == "" || isDefaultAccountid(accountId) {
|
|
r.logger.Error().Err(errNoPrimaryAccountFallback).Msg("failed to determine the accountId")
|
|
return "", apiError(r.errorId(), ErrorNonExistingAccount,
|
|
withDetail("Failed to determine the account to use"),
|
|
withSource(&ErrorSource{Parameter: UriParamAccountId}),
|
|
)
|
|
}
|
|
return accountId, nil
|
|
}
|
|
|
|
func (r Request) getAccountId(fallback string, err error) (string, *Error) {
|
|
accountId := chi.URLParam(r.r, UriParamAccountId)
|
|
if accountId == "" || isDefaultAccountid(accountId) {
|
|
accountId = fallback
|
|
}
|
|
if accountId == "" {
|
|
r.logger.Error().Err(err).Msg("failed to determine the accountId")
|
|
return "", apiError(r.errorId(), ErrorNonExistingAccount,
|
|
withDetail("Failed to determine the account to use"),
|
|
withSource(&ErrorSource{Parameter: UriParamAccountId}),
|
|
)
|
|
}
|
|
return accountId, nil
|
|
}
|
|
|
|
func (r Request) GetAccountIdForMail() (string, *Error) {
|
|
return r.getAccountId(r.session.PrimaryAccounts.Mail, errNoPrimaryAccountForMail)
|
|
}
|
|
|
|
func (r Request) GetAccountIdForBlob() (string, *Error) {
|
|
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForBlob)
|
|
}
|
|
|
|
func (r Request) GetAccountIdForVacationResponse() (string, *Error) {
|
|
return r.getAccountId(r.session.PrimaryAccounts.VacationResponse, errNoPrimaryAccountForVacationResponse)
|
|
}
|
|
|
|
func (r Request) GetAccountIdForQuota() (string, *Error) {
|
|
return r.getAccountId(r.session.PrimaryAccounts.Quota, errNoPrimaryAccountForQuota)
|
|
}
|
|
|
|
func (r Request) GetAccountIdForSubmission() (string, *Error) {
|
|
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForSubmission)
|
|
}
|
|
|
|
func (r Request) GetAccountIdForTask() (string, *Error) {
|
|
// TODO we don't have these yet, not implemented in Stalwart
|
|
// return r.getAccountId(r.session.PrimaryAccounts.Task, errNoPrimaryAccountForTask)
|
|
return r.GetAccountIdForMail()
|
|
}
|
|
|
|
func (r Request) GetAccountIdForCalendar() (string, *Error) {
|
|
// TODO we don't have these yet, not implemented in Stalwart
|
|
// return r.getAccountId(r.session.PrimaryAccounts.Calendar, errNoPrimaryAccountForCalendar)
|
|
return r.GetAccountIdForMail()
|
|
}
|
|
|
|
func (r Request) GetAccountIdForContact() (string, *Error) {
|
|
// TODO we don't have these yet, not implemented in Stalwart
|
|
// return r.getAccountId(r.session.PrimaryAccounts.Contact, errNoPrimaryAccountForContact)
|
|
return r.GetAccountIdForMail()
|
|
}
|
|
|
|
func (r Request) GetAccountForMail() (string, jmap.Account, *Error) {
|
|
accountId, err := r.GetAccountIdForMail()
|
|
if err != nil {
|
|
return "", jmap.Account{}, err
|
|
}
|
|
|
|
account, ok := r.session.Accounts[accountId]
|
|
if !ok {
|
|
r.logger.Debug().Msgf("failed to find account '%v'", accountId)
|
|
// TODO metric for inexistent accounts
|
|
return accountId, jmap.Account{}, apiError(r.errorId(), ErrorNonExistingAccount,
|
|
withDetail(fmt.Sprintf("The account '%v' does not exist", log.SafeString(accountId))),
|
|
withSource(&ErrorSource{Parameter: UriParamAccountId}),
|
|
)
|
|
}
|
|
return accountId, account, nil
|
|
}
|
|
|
|
func (r Request) parameterError(param string, detail string) *Error {
|
|
return r.observedParameterError(ErrorInvalidRequestParameter,
|
|
withDetail(detail),
|
|
withSource(&ErrorSource{Parameter: param}))
|
|
}
|
|
|
|
func (r Request) parameterErrorResponse(accountIds []string, param string, detail string) Response {
|
|
return errorResponse(accountIds, r.parameterError(param, detail))
|
|
}
|
|
|
|
func (r Request) getStringParam(param string, defaultValue string) (string, bool) {
|
|
q := r.r.URL.Query()
|
|
if !q.Has(param) {
|
|
return defaultValue, false
|
|
}
|
|
str := q.Get(param)
|
|
if str == "" {
|
|
return defaultValue, false
|
|
}
|
|
return str, true
|
|
}
|
|
|
|
func (r Request) getMandatoryStringParam(param string) (string, *Error) {
|
|
str := ""
|
|
q := r.r.URL.Query()
|
|
if q.Has(param) {
|
|
str = q.Get(param)
|
|
}
|
|
if str == "" {
|
|
msg := fmt.Sprintf("Missing required value for query parameter '%v'", param)
|
|
return "", r.observedParameterError(ErrorMissingMandatoryRequestParameter,
|
|
withDetail(msg),
|
|
withSource(&ErrorSource{Parameter: param}),
|
|
)
|
|
}
|
|
return str, nil
|
|
}
|
|
|
|
func (r Request) parseIntParam(param string, defaultValue int) (int, bool, *Error) {
|
|
q := r.r.URL.Query()
|
|
if !q.Has(param) {
|
|
return defaultValue, false, nil
|
|
}
|
|
|
|
str := q.Get(param)
|
|
if str == "" {
|
|
return defaultValue, false, nil
|
|
}
|
|
|
|
value, err := strconv.ParseInt(str, 10, 0)
|
|
if err != nil {
|
|
// don't include the original error, as it leaks too much about our implementation, e.g.:
|
|
// strconv.ParseInt: parsing \"a\": invalid syntax
|
|
msg := fmt.Sprintf("Invalid numeric value for query parameter '%v': '%s'", param, log.SafeString(str))
|
|
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
|
withDetail(msg),
|
|
withSource(&ErrorSource{Parameter: param}),
|
|
)
|
|
}
|
|
return int(value), true, nil
|
|
}
|
|
|
|
func (r Request) parseUIntParam(param string, defaultValue uint) (uint, bool, *Error) {
|
|
q := r.r.URL.Query()
|
|
if !q.Has(param) {
|
|
return defaultValue, false, nil
|
|
}
|
|
|
|
str := q.Get(param)
|
|
if str == "" {
|
|
return defaultValue, false, nil
|
|
}
|
|
|
|
value, err := strconv.ParseUint(str, 10, 0)
|
|
if err != nil {
|
|
// don't include the original error, as it leaks too much about our implementation, e.g.:
|
|
// strconv.ParseInt: parsing \"a\": invalid syntax
|
|
msg := fmt.Sprintf("Invalid numeric value for query parameter '%v': '%s'", param, log.SafeString(str))
|
|
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
|
withDetail(msg),
|
|
withSource(&ErrorSource{Parameter: param}),
|
|
)
|
|
}
|
|
return uint(value), true, nil
|
|
}
|
|
|
|
func (r Request) parseDateParam(param string) (time.Time, bool, *Error) {
|
|
q := r.r.URL.Query()
|
|
if !q.Has(param) {
|
|
return time.Time{}, false, nil
|
|
}
|
|
|
|
str := q.Get(param)
|
|
if str == "" {
|
|
return time.Time{}, false, nil
|
|
}
|
|
|
|
t, err := time.Parse(time.RFC3339, str)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("Invalid RFC3339 value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
|
return time.Time{}, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
|
withDetail(msg),
|
|
withSource(&ErrorSource{Parameter: param}),
|
|
)
|
|
}
|
|
return t, true, nil
|
|
}
|
|
|
|
func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *Error) {
|
|
q := r.r.URL.Query()
|
|
if !q.Has(param) {
|
|
return defaultValue, false, nil
|
|
}
|
|
|
|
str := q.Get(param)
|
|
if str == "" {
|
|
return defaultValue, false, nil
|
|
}
|
|
|
|
b, err := strconv.ParseBool(str)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("Invalid boolean value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
|
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
|
withDetail(msg),
|
|
withSource(&ErrorSource{Parameter: param}),
|
|
)
|
|
}
|
|
return b, true, nil
|
|
}
|
|
|
|
func (r Request) parseMapParam(param string) (map[string]string, bool, *Error) {
|
|
q := r.r.URL.Query()
|
|
if !q.Has(param) {
|
|
return map[string]string{}, false, nil
|
|
}
|
|
|
|
result := map[string]string{}
|
|
prefix := param + "."
|
|
for name, values := range q {
|
|
if strings.HasPrefix(name, prefix) {
|
|
if len(values) > 0 {
|
|
key := name[len(prefix)+1:]
|
|
result[key] = values[0]
|
|
}
|
|
}
|
|
}
|
|
return result, true, nil
|
|
}
|
|
|
|
func (r Request) body(target any) *Error {
|
|
body := r.r.Body
|
|
defer func(b io.ReadCloser) {
|
|
err := b.Close()
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("failed to close request body")
|
|
}
|
|
}(body)
|
|
|
|
err := json.NewDecoder(body).Decode(target)
|
|
if err != nil {
|
|
r.logger.Warn().Msgf("failed to deserialize the request body: %s", err.Error())
|
|
return r.observedParameterError(ErrorInvalidRequestBody, withSource(&ErrorSource{Pointer: "/"})) // we don't get any details here
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r Request) language() string {
|
|
return r.r.Header.Get("Accept-Language")
|
|
}
|
|
|
|
func (r Request) observe(obs prometheus.Observer, value float64) {
|
|
metrics.WithExemplar(obs, value, r.GetRequestId(), r.GetTraceId())
|
|
}
|
|
|
|
func (r Request) observeParameterError(err *Error) *Error {
|
|
if err != nil {
|
|
r.g.metrics.ParameterErrorCounter.WithLabelValues(err.Code).Inc()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r Request) observeJmapError(jerr jmap.Error) jmap.Error {
|
|
if jerr != nil {
|
|
r.g.metrics.JmapErrorCounter.WithLabelValues(r.session.JmapEndpoint, strconv.Itoa(jerr.Code())).Inc()
|
|
}
|
|
return jerr
|
|
}
|
|
|
|
func (r Request) needTask(accountId string) (bool, Response) {
|
|
if !IgnoreSessionCapabilityChecks {
|
|
if r.session.Capabilities.Tasks == nil {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingTasksSessionCapability), r.session.State)
|
|
}
|
|
}
|
|
return true, Response{}
|
|
}
|
|
|
|
func (r Request) needTaskForAccount(accountId string) (bool, Response) {
|
|
if ok, resp := r.needTask(accountId); !ok {
|
|
return ok, resp
|
|
}
|
|
account, ok := r.session.Accounts[accountId]
|
|
if !ok {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State)
|
|
}
|
|
if !IgnoreSessionCapabilityChecks {
|
|
if account.AccountCapabilities.Tasks == nil {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingTasksAccountCapability), r.session.State)
|
|
}
|
|
}
|
|
return true, Response{}
|
|
}
|
|
|
|
func (r Request) needTaskWithAccount() (bool, string, Response) {
|
|
accountId, err := r.GetAccountIdForTask()
|
|
if err != nil {
|
|
return false, "", errorResponse(single(accountId), err)
|
|
}
|
|
if !IgnoreSessionCapabilityChecks {
|
|
if ok, resp := r.needTaskForAccount(accountId); !ok {
|
|
return false, accountId, resp
|
|
}
|
|
}
|
|
return true, accountId, Response{}
|
|
}
|
|
|
|
func (r Request) needCalendar(accountId string) (bool, Response) {
|
|
if !IgnoreSessionCapabilityChecks {
|
|
if r.session.Capabilities.Calendars == nil {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State)
|
|
}
|
|
}
|
|
return true, Response{}
|
|
}
|
|
|
|
func (r Request) needCalendarForAccount(accountId string) (bool, Response) {
|
|
if ok, resp := r.needCalendar(accountId); !ok {
|
|
return ok, resp
|
|
}
|
|
account, ok := r.session.Accounts[accountId]
|
|
if !ok {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State)
|
|
}
|
|
if !IgnoreSessionCapabilityChecks {
|
|
if account.AccountCapabilities.Calendars == nil {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State)
|
|
}
|
|
}
|
|
return true, Response{}
|
|
}
|
|
|
|
func (r Request) needCalendarWithAccount() (bool, string, Response) {
|
|
accountId, err := r.GetAccountIdForCalendar()
|
|
if err != nil {
|
|
return false, "", errorResponse(single(accountId), err)
|
|
}
|
|
if !IgnoreSessionCapabilityChecks {
|
|
if ok, resp := r.needCalendarForAccount(accountId); !ok {
|
|
return false, accountId, resp
|
|
}
|
|
}
|
|
return true, accountId, Response{}
|
|
}
|
|
|
|
func (r Request) needContact(accountId string) (bool, Response) {
|
|
if r.session.Capabilities.Contacts == nil {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingContactsSessionCapability), r.session.State)
|
|
}
|
|
return true, Response{}
|
|
}
|
|
|
|
func (r Request) needContactForAccount(accountId string) (bool, Response) {
|
|
if ok, resp := r.needContact(accountId); !ok {
|
|
return ok, resp
|
|
}
|
|
account, ok := r.session.Accounts[accountId]
|
|
if !ok {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State)
|
|
}
|
|
if account.AccountCapabilities.Contacts == nil {
|
|
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingContactsAccountCapability), r.session.State)
|
|
}
|
|
return true, Response{}
|
|
}
|
|
|
|
func (r Request) needContactWithAccount() (bool, string, Response) {
|
|
accountId, err := r.GetAccountIdForContact()
|
|
if err != nil {
|
|
return false, "", errorResponse(single(accountId), err)
|
|
}
|
|
if ok, resp := r.needContactForAccount(accountId); !ok {
|
|
return false, accountId, resp
|
|
}
|
|
return true, accountId, Response{}
|
|
}
|