groupware: add identity deletion

This commit is contained in:
Pascal Bleser
2025-10-17 16:02:10 +02:00
parent 90e8470fbb
commit 3c386dfd1d
6 changed files with 153 additions and 5 deletions

View File

@@ -199,3 +199,32 @@ func (j *Client) UpdateIdentity(accountId string, session *Session, ctx context.
return response.NewState, nil
})
}
type IdentityDeletion struct {
Destroyed []string `json:"destroyed"`
NewState State `json:"newState,omitempty"`
}
func (j *Client) DeleteIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (IdentityDeletion, SessionState, Language, Error) {
logger = j.logger("DeleteIdentity", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{
AccountId: accountId,
Destroy: ids,
}, "0"))
if err != nil {
return IdentityDeletion{}, "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (IdentityDeletion, Error) {
var response IdentitySetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentitySet, "0", &response)
if err != nil {
return IdentityDeletion{}, err
}
for _, setErr := range response.NotDestroyed {
// TODO only returning the first error here, we should probably aggregate them instead
logger.Error().Msgf("%T.NotCreated returned an error %v", response, setErr)
return IdentityDeletion{}, setErrorError(setErr, IdentityType)
}
return IdentityDeletion{Destroyed: response.Destroyed, NewState: response.NewState}, nil
})
}

View File

@@ -67,6 +67,26 @@ func Map[E any, R any](source []E, indexer func(E) R) []R {
return result
}
func ToBoolMap[E comparable](source []E) map[E]bool {
m := make(map[E]bool, len(source))
for _, v := range source {
m[v] = true
}
return m
}
func ToIntMap[E comparable](source []E) map[E]int {
m := make(map[E]int, len(source))
for _, v := range source {
if e, ok := m[v]; ok {
m[v] = e + 1
} else {
m[v] = 1
}
}
return m
}
func MapN[E any, R any](source []E, indexer func(E) *R) []R {
if source == nil {
var zero []R
@@ -81,3 +101,39 @@ func MapN[E any, R any](source []E, indexer func(E) *R) []R {
}
return result
}
// Check whether two slices contain the same elements, ignoring order.
func SameSlices[E comparable](x, y []E) bool {
// https://stackoverflow.com/a/36000696
if len(x) != len(y) {
return false
}
// create a map of string -> int
diff := make(map[E]int, len(x))
for _, _x := range x {
// 0 value for int is 0, so just increment a counter for the string
diff[_x]++
}
for _, _y := range y {
// If the string _y is not in diff bail out early
if _, ok := diff[_y]; !ok {
return false
}
diff[_y]--
if diff[_y] == 0 {
delete(diff, _y)
}
}
return len(diff) == 0
}
func Missing[E comparable](expected, actual []E) []E {
missing := []E{}
actualIndex := ToBoolMap(actual)
for _, e := range expected {
if _, ok := actualIndex[e]; !ok {
missing = append(missing, e)
}
}
return missing
}

View File

@@ -2,6 +2,7 @@ package structs
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -99,3 +100,24 @@ func TestKeys(t *testing.T) {
})
}
}
func TestMissing(t *testing.T) {
tests := []struct {
source []string
input []string
expected []string
}{
{[]string{"a", "b", "c"}, []string{"c", "b", "a"}, []string{}},
{[]string{"a", "b", "c"}, []string{"c", "b"}, []string{"a"}},
{[]string{"a", "b", "c"}, []string{"c", "b", "a", "d"}, []string{}},
{[]string{}, []string{"c", "b"}, []string{}},
{[]string{"a", "b", "c"}, []string{}, []string{"a", "b", "c"}},
{[]string{"a", "b", "b", "c"}, []string{"a", "b"}, []string{"c"}},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("%d: testing [%v] <-> [%v] == [%v]", i+1, strings.Join(tt.source, ", "), strings.Join(tt.input, ", "), strings.Join(tt.expected, ", ")), func(t *testing.T) {
result := Missing(tt.source, tt.input)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -1,11 +1,14 @@
package groupware
import (
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
// When the request suceeds.
@@ -43,7 +46,7 @@ func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdWithoutFallback()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(err)
}
@@ -62,7 +65,7 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) AddIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdWithoutFallback()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(err)
}
@@ -84,7 +87,7 @@ func (g *Groupware) AddIdentity(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdWithoutFallback()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(err)
}
@@ -103,3 +106,33 @@ func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
return noContentResponseWithEtag(sessionState, newState)
})
}
func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
id := chi.URLParam(r, UriParamIdentityId)
ids := strings.Split(id, ",")
if len(ids) < 1 {
return req.parameterErrorResponse(UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
}
deletion, sessionState, _, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
notDeletedIds := structs.Missing(ids, deletion.Destroyed)
if len(notDeletedIds) == 0 {
return noContentResponseWithEtag(sessionState, deletion.NewState)
} else {
logger.Error().Array("not-deleted", log.SafeStringArray(notDeletedIds)).Msgf("failed to delete %d identities", len(notDeletedIds))
return errorResponseWithSessionState(req.apiError(&ErrorFailedToDeleteSomeIdentities,
withMeta(map[string]any{"ids": notDeletedIds})), sessionState)
}
})
}

View File

@@ -195,6 +195,7 @@ const (
ErrorCodeMissingTasksSessionCapability = "MSCTSK"
ErrorCodeMissingTaskAccountCapability = "MACTSK"
ErrorCodeFailedToDeleteEmail = "DELEML"
ErrorCodeFailedToDeleteSomeIdentities = "DELSID"
)
var (
@@ -420,6 +421,12 @@ var (
Title: "Failed to delete emails",
Detail: "One or more emails could not be deleted.",
}
ErrorFailedToDeleteSomeIdentities = GroupwareError{
Status: http.StatusInternalServerError,
Code: ErrorCodeFailedToDeleteSomeIdentities,
Title: "Failed to delete some Identities",
Detail: "Failed to delete some or all of the identities.",
}
)
type ErrorOpt interface {
@@ -576,12 +583,12 @@ func (r Request) observedParameterError(gwerr GroupwareError, options ...ErrorOp
return r.observeParameterError(apiError(r.errorId(), gwerr, options...))
}
func (r Request) apiError(err *GroupwareError) *Error {
func (r Request) apiError(err *GroupwareError, options ...ErrorOpt) *Error {
if err == nil {
return nil
}
errorId := r.errorId()
return apiError(errorId, *err)
return apiError(errorId, *err, options...)
}
func (r Request) apiErrorFromJmap(err jmap.Error) *Error {

View File

@@ -79,6 +79,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/{identityid}", g.GetIdentityById)
r.Post("/", g.AddIdentity)
r.Patch("/{identityid}", g.ModifyIdentity)
r.Delete("/{identityid}", g.DeleteIdentity)
})
r.Get("/vacation", g.GetVacation)
r.Put("/vacation", g.SetVacation)