mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-22 12:59:23 -05:00
groupware: add identity deletion
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user