From 667d82f92c77d1997b1a65d37b9dedd211eabfac Mon Sep 17 00:00:00 2001 From: "A. Diamond" Date: Mon, 22 Jan 2024 15:59:25 -0500 Subject: [PATCH] Working on permissions for bulk deletion --- constants/permissions.go | 3 + middleware/authorization_map.go | 97 ++++++++++--------- .../admin/intellectual_objects_controller.go | 10 +- .../intellectual_objects_controller_test.go | 32 ++++++ web/webui/deletion.go | 15 ++- 5 files changed, 105 insertions(+), 52 deletions(-) diff --git a/constants/permissions.go b/constants/permissions.go index 6bc1d1c..e1558a4 100644 --- a/constants/permissions.go +++ b/constants/permissions.go @@ -47,6 +47,7 @@ const ( InstitutionRead = "InstitutionRead" InstitutionUpdate = "InstitutionUpdate" InstitutionUpdatePrefs = "InstitutionUpdatePrefs" + IntellectualObjectBatchDelete = "IntellectualObjectBatchDelete" IntellectualObjectCreate = "IntellectualObjectCreate" IntellectualObjectDelete = "IntellectualObjectDelete" IntellectualObjectFinishBulkDelete = "IntellectualObjectFinishBulkDelete" @@ -124,6 +125,7 @@ var Permissions = []Permission{ InstitutionRead, InstitutionUpdate, InstitutionUpdatePrefs, + IntellectualObjectBatchDelete, IntellectualObjectCreate, IntellectualObjectDelete, IntellectualObjectFinishBulkDelete, @@ -311,6 +313,7 @@ func initPermissions() { sysAdmin[InstitutionRead] = true sysAdmin[InstitutionUpdate] = true sysAdmin[InstitutionUpdatePrefs] = true + sysAdmin[IntellectualObjectBatchDelete] = true sysAdmin[IntellectualObjectCreate] = true sysAdmin[IntellectualObjectDelete] = true // preserv workers do this with sys admin account sysAdmin[IntellectualObjectFinishBulkDelete] = true // not implemented yet diff --git a/middleware/authorization_map.go b/middleware/authorization_map.go index 275e285..57d466d 100644 --- a/middleware/authorization_map.go +++ b/middleware/authorization_map.go @@ -31,54 +31,55 @@ type AuthMetadata struct { // requests that hit an unguarded route will return an internal server // error. var AuthMap = map[string]AuthMetadata{ - "AlertCreate": {"Alert", constants.AlertCreate}, - "AlertDelete": {"Alert", constants.AlertDelete}, - "AlertIndex": {"Alert", constants.AlertRead}, - "AlertNew": {"Alert", constants.AlertCreate}, - "AlertShow": {"Alert", constants.AlertRead}, - "AlertUpdate": {"Alert", constants.AlertUpdate}, - "AlertMarkAsReadXHR": {"Alert", constants.AlertUpdate}, - "AlertMarkAllAsRead": {"Alert", constants.AlertUpdate}, - "AlertMarkAsUnreadXHR": {"Alert", constants.AlertUpdate}, - "BillingReportShow": {"DepositStats", constants.BillingReportShow}, - "ChecksumCreate": {"Checksum", constants.ChecksumCreate}, - "ChecksumDelete": {"Checksum", constants.ChecksumDelete}, - "ChecksumIndex": {"Checksum", constants.ChecksumRead}, - "ChecksumNew": {"Checksum", constants.ChecksumCreate}, - "ChecksumShow": {"Checksum", constants.ChecksumRead}, - "ChecksumUpdate": {"Checksum", constants.ChecksumUpdate}, - "DashboardShow": {"Dashboard", constants.DashboardShow}, - "DeletionRequestApprove": {"DeletionRequest", constants.DeletionRequestApprove}, - "DeletionRequestCancel": {"DeletionRequest", constants.DeletionRequestApprove}, - "DeletionRequestIndex": {"DeletionRequest", constants.DeletionRequestList}, - "DeletionRequestReview": {"DeletionRequest", constants.DeletionRequestApprove}, - "DeletionRequestShow": {"DeletionRequest", constants.DeletionRequestShow}, - "DepositReportShow": {"DepositStats", constants.DepositReportShow}, - "GenericFileCreate": {"GenericFile", constants.FileCreate}, - "GenericFileCreateBatch": {"GenericFile", constants.FileCreate}, - "GenericFileDelete": {"GenericFile", constants.FileDelete}, - "GenericFileFinishBulkDelete": {"GenericFile", constants.FileFinishBulkDelete}, - "GenericFileIndex": {"GenericFile", constants.FileRead}, - "GenericFileInitDelete": {"GenericFile", constants.FileRequestDelete}, - "GenericFileInitRestore": {"GenericFile", constants.FileRestore}, - "GenericFileNew": {"GenericFile", constants.FileCreate}, - "GenericFileRequestDelete": {"GenericFile", constants.FileRequestDelete}, - "GenericFileRequestRestore": {"GenericFile", constants.FileRestore}, - "GenericFileShow": {"GenericFile", constants.FileRead}, - "GenericFileUpdate": {"GenericFile", constants.FileUpdate}, - "InstitutionCreate": {"Institution", constants.InstitutionCreate}, - "InstitutionDelete": {"Institution", constants.InstitutionDelete}, - "InstitutionEdit": {"Institution", constants.InstitutionUpdate}, - "InstitutionEditPrefs": {"Institution", constants.InstitutionUpdatePrefs}, - "InstitutionIndex": {"Institution", constants.InstitutionList}, - "InstitutionNew": {"Institution", constants.InstitutionCreate}, - "InstitutionShow": {"Institution", constants.InstitutionRead}, - "InstitutionUndelete": {"Institution", constants.InstitutionUpdate}, - "InstitutionUpdate": {"Institution", constants.InstitutionUpdate}, - "InstitutionUpdatePrefs": {"Institution", constants.InstitutionUpdatePrefs}, - "IntellectualObjectCreate": {"IntellectualObject", constants.IntellectualObjectCreate}, - "IntellectualObjectDelete": {"IntellectualObject", constants.IntellectualObjectDelete}, - "IntellectualObjectEvents": {"PremisEvent", constants.EventRead}, + "AlertCreate": {"Alert", constants.AlertCreate}, + "AlertDelete": {"Alert", constants.AlertDelete}, + "AlertIndex": {"Alert", constants.AlertRead}, + "AlertNew": {"Alert", constants.AlertCreate}, + "AlertShow": {"Alert", constants.AlertRead}, + "AlertUpdate": {"Alert", constants.AlertUpdate}, + "AlertMarkAsReadXHR": {"Alert", constants.AlertUpdate}, + "AlertMarkAllAsRead": {"Alert", constants.AlertUpdate}, + "AlertMarkAsUnreadXHR": {"Alert", constants.AlertUpdate}, + "BillingReportShow": {"DepositStats", constants.BillingReportShow}, + "ChecksumCreate": {"Checksum", constants.ChecksumCreate}, + "ChecksumDelete": {"Checksum", constants.ChecksumDelete}, + "ChecksumIndex": {"Checksum", constants.ChecksumRead}, + "ChecksumNew": {"Checksum", constants.ChecksumCreate}, + "ChecksumShow": {"Checksum", constants.ChecksumRead}, + "ChecksumUpdate": {"Checksum", constants.ChecksumUpdate}, + "DashboardShow": {"Dashboard", constants.DashboardShow}, + "DeletionRequestApprove": {"DeletionRequest", constants.DeletionRequestApprove}, + "DeletionRequestCancel": {"DeletionRequest", constants.DeletionRequestApprove}, + "DeletionRequestIndex": {"DeletionRequest", constants.DeletionRequestList}, + "DeletionRequestReview": {"DeletionRequest", constants.DeletionRequestApprove}, + "DeletionRequestShow": {"DeletionRequest", constants.DeletionRequestShow}, + "DepositReportShow": {"DepositStats", constants.DepositReportShow}, + "GenericFileCreate": {"GenericFile", constants.FileCreate}, + "GenericFileCreateBatch": {"GenericFile", constants.FileCreate}, + "GenericFileDelete": {"GenericFile", constants.FileDelete}, + "GenericFileFinishBulkDelete": {"GenericFile", constants.FileFinishBulkDelete}, + "GenericFileIndex": {"GenericFile", constants.FileRead}, + "GenericFileInitDelete": {"GenericFile", constants.FileRequestDelete}, + "GenericFileInitRestore": {"GenericFile", constants.FileRestore}, + "GenericFileNew": {"GenericFile", constants.FileCreate}, + "GenericFileRequestDelete": {"GenericFile", constants.FileRequestDelete}, + "GenericFileRequestRestore": {"GenericFile", constants.FileRestore}, + "GenericFileShow": {"GenericFile", constants.FileRead}, + "GenericFileUpdate": {"GenericFile", constants.FileUpdate}, + "InstitutionCreate": {"Institution", constants.InstitutionCreate}, + "InstitutionDelete": {"Institution", constants.InstitutionDelete}, + "InstitutionEdit": {"Institution", constants.InstitutionUpdate}, + "InstitutionEditPrefs": {"Institution", constants.InstitutionUpdatePrefs}, + "InstitutionIndex": {"Institution", constants.InstitutionList}, + "InstitutionNew": {"Institution", constants.InstitutionCreate}, + "InstitutionShow": {"Institution", constants.InstitutionRead}, + "InstitutionUndelete": {"Institution", constants.InstitutionUpdate}, + "InstitutionUpdate": {"Institution", constants.InstitutionUpdate}, + "InstitutionUpdatePrefs": {"Institution", constants.InstitutionUpdatePrefs}, + "IntellectualObjectInitBatchDelete": {"IntellectualObject", constants.IntellectualObjectBatchDelete}, + "IntellectualObjectCreate": {"IntellectualObject", constants.IntellectualObjectCreate}, + "IntellectualObjectDelete": {"IntellectualObject", constants.IntellectualObjectDelete}, + "IntellectualObjectEvents": {"PremisEvent", constants.EventRead}, // IntellectualObjectFiles gets an object ID and will look up that object to check // it's institution. The permission, however, is FileReade, because this endpoint // returns files. https://trello.com/c/n5asx3bj diff --git a/web/api/admin/intellectual_objects_controller.go b/web/api/admin/intellectual_objects_controller.go index 59c2d70..3c10c62 100644 --- a/web/api/admin/intellectual_objects_controller.go +++ b/web/api/admin/intellectual_objects_controller.go @@ -69,7 +69,15 @@ func IntellectualObjectInitBatchDelete(c *gin.Context) { if api.AbortIfError(c, err) { return } - del, err := webui.NewDeletionForObjectBatch(institutionID, objectIDs, req.CurrentUser, req.BaseURL()) + requestorID, err := strconv.ParseInt(c.Request.PostFormValue("requestorID"), 10, 64) + if api.AbortIfError(c, err) { + return + } + + common.Context().Log.Warn().Msgf("Creating batch deletion request on behalf of user %d for %d objects belonging to institution %d. Current user is %s.", + requestorID, len(objectIDs), institutionID, req.CurrentUser.Email) + + del, err := webui.NewDeletionForObjectBatch(requestorID, institutionID, objectIDs, req.BaseURL()) if api.AbortIfError(c, err) { return } diff --git a/web/api/admin/intellectual_objects_controller_test.go b/web/api/admin/intellectual_objects_controller_test.go index 009d509..6fe004d 100644 --- a/web/api/admin/intellectual_objects_controller_test.go +++ b/web/api/admin/intellectual_objects_controller_test.go @@ -291,3 +291,35 @@ func TestObjectInitRestore(t *testing.T) { WithHeader(constants.APIKeyHeader, "password"). Expect().Status(http.StatusForbidden) } + +func TestObjectBatchDelete(t *testing.T) { + // START HERE + + // Test permissions. Only APTrust admin should be allowed to do this. + + // Ensure that we get failure if we include an object + // with a pending WorkItem. + + // Ensure that we get failure if we include an object + // that belongs to another institution. + + // Ensure that we get failure if requestorID belongs + // to an inst user rather than inst admin. + + // Ensure we get success with valid params: + // inst id, user id, object ids. + + // Check post conditions. There should be a deletion + // request with all expected properties and with the + // right list of object IDs. + + // Check the text of the alert. It should include + // all of the object identifiers. + + // Confirm the alert, and then test that the correct + // WorkItems were created and that no spurious work + // items were created. + + // TODO: Create & test bulk delete ENV token from + // parameter store? +} diff --git a/web/webui/deletion.go b/web/webui/deletion.go index 0b448c9..8f79a64 100644 --- a/web/webui/deletion.go +++ b/web/webui/deletion.go @@ -88,7 +88,16 @@ func NewDeletionForObject(objID int64, currentUser *pgmodels.User, baseURL strin // IntellectualObjects and returns the Deletion object. This constructor // is only for initializing new DeletionRequests, not for reviewing, approving // or cancelling existing requests. -func NewDeletionForObjectBatch(institutionID int64, objIDs []int64, currentUser *pgmodels.User, baseURL string) (*Deletion, error) { +func NewDeletionForObjectBatch(requestorID, institutionID int64, objIDs []int64, baseURL string) (*Deletion, error) { + + requestingUser, err := pgmodels.UserByID(requestorID) + if err != nil { + return nil, err + } + if requestingUser.InstitutionID != institutionID || requestingUser.Role != constants.RoleInstAdmin { + common.Context().Log.Error().Msgf("Requesting user %d is not admin at institution %d. Rejecting bulk deletion request.", requestorID, institutionID) + return nil, fmt.Errorf("invalid requestor id") + } // Make sure that all objects belong to the specified institution. validObjectCount, err := pgmodels.CountObjectsThatCanBeDeleted(institutionID, objIDs) @@ -97,7 +106,7 @@ func NewDeletionForObjectBatch(institutionID int64, objIDs []int64, currentUser } if validObjectCount != len(objIDs) { common.Context().Log.Error().Msgf("Batch deletion requested for %d objects, of which only %d are valid. InstitutionID = %d. Current user = %s. IDs: %v", - len(objIDs), validObjectCount, institutionID, currentUser.Email, objIDs) + len(objIDs), validObjectCount, institutionID, requestingUser.Email, objIDs) return nil, fmt.Errorf("one or more object ids is invalid") } @@ -113,7 +122,7 @@ func NewDeletionForObjectBatch(institutionID int64, objIDs []int64, currentUser del := &Deletion{ baseURL: baseURL, - currentUser: currentUser, + currentUser: requestingUser, } err = del.initObjectDeletionRequest(institutionID, objIDs) if err != nil {