diff --git a/backend/go.mod b/backend/go.mod index 54e42ed..0542d5c 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -15,7 +15,7 @@ require ( github.com/go-chi/render v1.0.2 github.com/google/go-cmp v0.6.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/privacy-pal/privacy-pal/go v1.0.0 + github.com/privacy-pal/privacy-pal/go v1.3.0 github.com/rs/cors v1.8.3 golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb google.golang.org/grpc v1.58.3 diff --git a/backend/go.sum b/backend/go.sum index f25422a..5f08b9b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -90,6 +90,12 @@ github.com/privacy-pal/privacy-pal/go v0.6.0 h1:ShjRa0vO+CBPJgD/B1OjiIBfit8B8bu7 github.com/privacy-pal/privacy-pal/go v0.6.0/go.mod h1:iVFaK4o+EYLTOyetSv6pjtWGcmPcTMXL6oUNHVyhoX0= github.com/privacy-pal/privacy-pal/go v1.0.0 h1:Oti8tPCzAxKzqb9EPP6l1E4P5gJZbL10gHRkoEn7Kxo= github.com/privacy-pal/privacy-pal/go v1.0.0/go.mod h1:iVFaK4o+EYLTOyetSv6pjtWGcmPcTMXL6oUNHVyhoX0= +github.com/privacy-pal/privacy-pal/go v1.1.0 h1:Fm03J8kbOuuQxV8ayvf+NJSCdToQt0FI2r1SLKfzthA= +github.com/privacy-pal/privacy-pal/go v1.1.0/go.mod h1:4mjmDe2nLDBPMuhiOx1GIy04AQaBWzRUL1YOUZTEi0o= +github.com/privacy-pal/privacy-pal/go v1.2.0 h1:hGRYynIM7NScP/CJ8b2TY8SxndK+kc0kCvnyc3IcOGM= +github.com/privacy-pal/privacy-pal/go v1.2.0/go.mod h1:6wbYoDZiD/AxJI0K2/futdQKCrAR4BHxb7TDgdnOGBU= +github.com/privacy-pal/privacy-pal/go v1.3.0 h1:7Pn2IE62KVIcdhab5xIL+IdF0Tv7jmgoCMsh7iCMO5A= +github.com/privacy-pal/privacy-pal/go v1.3.0/go.mod h1:6wbYoDZiD/AxJI0K2/futdQKCrAR4BHxb7TDgdnOGBU= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= diff --git a/backend/pkg/privacy/assignment.go b/backend/pkg/privacy/assignment.go index 9e7d169..7f2f5b1 100644 --- a/backend/pkg/privacy/assignment.go +++ b/backend/pkg/privacy/assignment.go @@ -1,11 +1,12 @@ package privacy import ( + "cloud.google.com/go/firestore" pal "github.com/privacy-pal/privacy-pal/go/pkg" ) -func accessAssignment(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) map[string]interface{} { - data := map[string]interface{}{ +func handleAccessAssignment(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (data map[string]interface{}, err error) { + data = map[string]interface{}{ "name": dbObj["name"], "optional": dbObj["optional"], "maxScore": dbObj["maxScore"], @@ -14,5 +15,21 @@ func accessAssignment(dataSubjectId string, currentDbObjLocator pal.Locator, dbO "grade": dbObj["grades"].(map[string]interface{})[dataSubjectId], } - return data + return +} + +func handleDeleteAssignment(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (nodesToTraverse []pal.Locator, deleteNode bool, fieldsToUpdate pal.FieldUpdates, err error) { + deleteNode = false + + // remove grades[dataSubjectId] if exists + fieldsToUpdate = pal.FieldUpdates{ + FirestoreUpdates: []firestore.Update{ + { + Path: "grades." + dataSubjectId, + Value: firestore.Delete, + }, + }, + } + + return } diff --git a/backend/pkg/privacy/course.go b/backend/pkg/privacy/course.go index 7fd8a1e..5058d8d 100644 --- a/backend/pkg/privacy/course.go +++ b/backend/pkg/privacy/course.go @@ -1,12 +1,13 @@ package privacy import ( + "cloud.google.com/go/firestore" "github.com/fullstackatbrown/here/pkg/models" pal "github.com/privacy-pal/privacy-pal/go/pkg" ) -func accessCourse(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) map[string]interface{} { - data := map[string]interface{}{ +func handleAccessCourse(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (data map[string]interface{}, err error) { + data = map[string]interface{}{ "title": dbObj["title"], } @@ -45,5 +46,51 @@ func accessCourse(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj p }, } - return data + return +} + +func handleDeleteCourse(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (nodesToTraverse []pal.Locator, deleteNode bool, fieldsToUpdate pal.FieldUpdates, err error) { + deleteNode = false + + updates := []firestore.Update{} + // remove student associated with dataSubjectId from the students field + updates = append(updates, firestore.Update{ + Path: "students." + dataSubjectId, + Value: firestore.Delete, + }) + // remove permission associated with dataSubjectId from the permissions field + updates = append(updates, firestore.Update{ + Path: "permissions." + dataSubjectId, + Value: firestore.Delete, + }) + + // add swaps and surveys to nodesToTraverse + nodesToTraverse = append(nodesToTraverse, pal.Locator{ + LocatorType: pal.Collection, + DataType: SwapDataType, + FirestoreLocator: pal.FirestoreLocator{ + CollectionPath: []string{models.FirestoreCoursesCollection, models.FirestoreSwapsCollection}, + DocIDs: []string{currentDbObjLocator.FirestoreLocator.DocIDs[0]}, + Filters: []pal.Filter{ + { + Path: "studentID", + Op: "==", + Value: dataSubjectId, + }, + }, + }, + }) + nodesToTraverse = append(nodesToTraverse, pal.Locator{ + LocatorType: pal.Collection, + DataType: SurveyDataType, + FirestoreLocator: pal.FirestoreLocator{ + CollectionPath: []string{models.FirestoreCoursesCollection, models.FirestoreSurveysCollection}, + DocIDs: []string{currentDbObjLocator.FirestoreLocator.DocIDs[0]}, + }, + }) + + fieldsToUpdate = pal.FieldUpdates{ + FirestoreUpdates: updates, + } + return } diff --git a/backend/pkg/privacy/errors.go b/backend/pkg/privacy/errors.go new file mode 100644 index 0000000..4c4854c --- /dev/null +++ b/backend/pkg/privacy/errors.go @@ -0,0 +1,11 @@ +package privacy + +import "fmt" + +var ( + invalidLocatorDataType = fmt.Errorf("invalid locator data type") +) + +func cannotCastFieldToType(field string, castType string) error { + return fmt.Errorf("cannot cast %s to %s", field, castType) +} diff --git a/backend/pkg/privacy/privacy.go b/backend/pkg/privacy/privacy.go index b268fd2..1fd65cc 100644 --- a/backend/pkg/privacy/privacy.go +++ b/backend/pkg/privacy/privacy.go @@ -13,22 +13,42 @@ const ( SwapDataType = "swap" ) -func HandleAccess(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) map[string]interface{} { +func HandleAccess(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (data map[string]interface{}, err error) { switch currentDbObjLocator.DataType { case UserDataType: - return accessUser(dataSubjectId, currentDbObjLocator, dbObj) + return handleAccessUser(dataSubjectId, currentDbObjLocator, dbObj) case CourseDataType: - return accessCourse(dataSubjectId, currentDbObjLocator, dbObj) + return handleAccessCourse(dataSubjectId, currentDbObjLocator, dbObj) case SectionDataType: - return accessSection(dataSubjectId, currentDbObjLocator, dbObj) + return handleAccessSection(dataSubjectId, currentDbObjLocator, dbObj) case AssignmentDataType: - return accessAssignment(dataSubjectId, currentDbObjLocator, dbObj) + return handleAccessAssignment(dataSubjectId, currentDbObjLocator, dbObj) case SurveyDataType: - return accessSurvey(dataSubjectId, currentDbObjLocator, dbObj) + return handleAccessSurvey(dataSubjectId, currentDbObjLocator, dbObj) case SwapDataType: - return accessSwap(dataSubjectId, currentDbObjLocator, dbObj) + return handleAccessSwap(dataSubjectId, currentDbObjLocator, dbObj) default: - // TODO: should return error - return nil + err = invalidLocatorDataType + return + } +} + +func HandleDelete(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (nodesToTraverse []pal.Locator, deleteNode bool, fieldsToUpdate pal.FieldUpdates, err error) { + switch currentDbObjLocator.DataType { + case UserDataType: + return handleDeleteUser(dataSubjectId, currentDbObjLocator, dbObj) + case CourseDataType: + return handleDeleteCourse(dataSubjectId, currentDbObjLocator, dbObj) + case SectionDataType: + return handleDeleteSection(dataSubjectId, currentDbObjLocator, dbObj) + case AssignmentDataType: + return handleDeleteAssignment(dataSubjectId, currentDbObjLocator, dbObj) + case SurveyDataType: + return handleDeleteSurvey(dataSubjectId, currentDbObjLocator, dbObj) + case SwapDataType: + return handleDeleteSwap(dataSubjectId, currentDbObjLocator, dbObj) + default: + err = invalidLocatorDataType + return } } diff --git a/backend/pkg/privacy/section.go b/backend/pkg/privacy/section.go index 6636fe1..ca66032 100644 --- a/backend/pkg/privacy/section.go +++ b/backend/pkg/privacy/section.go @@ -1,15 +1,90 @@ package privacy import ( + "fmt" + + "cloud.google.com/go/firestore" pal "github.com/privacy-pal/privacy-pal/go/pkg" ) -func accessSection(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) map[string]interface{} { - data := map[string]interface{}{ +func handleAccessSection(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (data map[string]interface{}, err error) { + data = map[string]interface{}{ "startTime": dbObj["startTime"], "endTime": dbObj["endTime"], "location": dbObj["location"], } - return data + return +} + +func handleDeleteSection(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (nodesToTraverse []pal.Locator, deleteNode bool, fieldsToUpdate pal.FieldUpdates, err error) { + deleteNode = false + + // decrement numEnrolled in course + updates := []firestore.Update{ + { + Path: "numEnrolled", + Value: firestore.Increment(-1), + }, + } + + // remove dataSubjectId from swappedInStudents if exists + swappedInStudents, ok := dbObj["swappedInStudents"].(map[string]interface{}) + if !ok { + err = cannotCastFieldToType("swappedInStudents", "map[string]interface{}") + return + } + for _, studentIDs := range swappedInStudents { + studentIDsSlice, ok := studentIDs.([]interface{}) + if !ok { + err = cannotCastFieldToType(fmt.Sprintf("swappedInStudents entry %v", studentIDs), "[]interface{}") + return + } + for _, studentID := range studentIDsSlice { + id, ok := studentID.(string) + if !ok { + err = cannotCastFieldToType(fmt.Sprintf("swappedInStudents nested entry %v", studentID), "string") + return + } + if id == dataSubjectId { + updates = append(updates, firestore.Update{ + Path: "swappedInStudents." + dataSubjectId, + Value: firestore.Delete, + }) + } + } + } + + // remove dataSubjectId from swappedOutStudents if exists + swappedOutStudents, ok := dbObj["swappedOutStudents"].(map[string]interface{}) + if !ok { + err = cannotCastFieldToType("swappedOutStudents", "map[string]interface{}") + return + } + for _, studentIDs := range swappedOutStudents { + studentIDsSlice, ok := studentIDs.([]interface{}) + if !ok { + err = cannotCastFieldToType(fmt.Sprintf("swappedOutStudents entry %v", studentIDs), "[]interface{}") + return + } + for _, studentID := range studentIDsSlice { + id, ok := studentID.(string) + if !ok { + err = cannotCastFieldToType(fmt.Sprintf("swappedOutStudents nested entry %v", studentID), "string") + return + } + if id == dataSubjectId { + updates = append(updates, firestore.Update{ + Path: "swappedOutStudents." + dataSubjectId, + Value: firestore.Delete, + }) + } + } + } + + fieldsToUpdate = pal.FieldUpdates{ + FirestoreUpdates: updates, + } + + return } diff --git a/backend/pkg/privacy/survey.go b/backend/pkg/privacy/survey.go index 332501c..ce8f073 100644 --- a/backend/pkg/privacy/survey.go +++ b/backend/pkg/privacy/survey.go @@ -1,11 +1,14 @@ package privacy import ( + "fmt" + + "cloud.google.com/go/firestore" pal "github.com/privacy-pal/privacy-pal/go/pkg" ) -func accessSurvey(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) map[string]interface{} { - data := map[string]interface{}{ +func handleAccessSurvey(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (data map[string]interface{}, err error) { + data = map[string]interface{}{ "name": dbObj["name"], "description": dbObj["description"], "endTime": dbObj["endTime"], @@ -13,5 +16,48 @@ func accessSurvey(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj p "responses": dbObj["responses"].(map[string]interface{})[dataSubjectId], } - return data + return +} + +func handleDeleteSurvey(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (nodesToTraverse []pal.Locator, deleteNode bool, fieldsToUpdate pal.FieldUpdates, err error) { + deleteNode = false + + // remove responses[dataSubjectId] if exists + updates := []firestore.Update{ + { + Path: "responses." + dataSubjectId, + Value: firestore.Delete, + }, + } + + // remove the result of dataSubjectId from each entry in results + results, ok := dbObj["results"].(map[string]interface{}) + if ok { // result could be nil if no results + for option, data := range results { + courseUserData, ok := data.([]interface{}) + if !ok { + err = fmt.Errorf("courseUserData is not a []interface{}") + return + } + for _, data := range courseUserData { + cud, ok := data.(map[string]interface{}) + if !ok { + err = fmt.Errorf("cud is not a map[string]interface{}") + return + } + if cud["studentID"] == dataSubjectId { + updates = append(updates, firestore.Update{ + Path: "results." + option, + Value: firestore.ArrayRemove(data), + }) + } + } + } + } + + fieldsToUpdate = pal.FieldUpdates{ + FirestoreUpdates: updates, + } + + return } diff --git a/backend/pkg/privacy/swap.go b/backend/pkg/privacy/swap.go index 214c19b..bb38676 100644 --- a/backend/pkg/privacy/swap.go +++ b/backend/pkg/privacy/swap.go @@ -4,8 +4,8 @@ import ( pal "github.com/privacy-pal/privacy-pal/go/pkg" ) -func accessSwap(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) map[string]interface{} { - data := map[string]interface{}{ +func handleAccessSwap(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (data map[string]interface{}, err error) { + data = map[string]interface{}{ "oldSectionID": dbObj["oldSectionID"], "newSectionID": dbObj["newSectionID"], "assignmentID": dbObj["assignmentID"], @@ -16,5 +16,10 @@ func accessSwap(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal "handledBy": dbObj["handledBy"], } - return data + return +} + +func handleDeleteSwap(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (nodesToTraverse []pal.Locator, deleteNode bool, fieldsToUpdate pal.FieldUpdates, err error) { + deleteNode = true + return } diff --git a/backend/pkg/privacy/user.go b/backend/pkg/privacy/user.go index e327259..f020609 100644 --- a/backend/pkg/privacy/user.go +++ b/backend/pkg/privacy/user.go @@ -1,12 +1,14 @@ package privacy import ( + "fmt" + "github.com/fullstackatbrown/here/pkg/models" pal "github.com/privacy-pal/privacy-pal/go/pkg" ) -func accessUser(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) map[string]interface{} { - data := map[string]interface{}{ +func handleAccessUser(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (data map[string]interface{}, err error) { + data = map[string]interface{}{ "name": dbObj["displayName"], "email": dbObj["email"], "photoUrl": dbObj["photoUrl"], @@ -16,16 +18,15 @@ func accessUser(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal slice, ok := dbObj["courses"].([]interface{}) if !ok { - return data + err = cannotCastFieldToType("courses", "[]interface{}") + return } courses := make([]string, len(slice)) - if !ok { - return data - } for i, v := range slice { course, ok := v.(string) if !ok { - return data + err = cannotCastFieldToType(fmt.Sprintf("courses element %v", v), "string") + return } courses[i] = course } @@ -45,7 +46,8 @@ func accessUser(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal sections, ok := dbObj["defaultSections"].(map[string]interface{}) if !ok { - return data + err = cannotCastFieldToType("defaultSections", "map[string]interface{}") + return } data["defaultSections"] = []pal.Locator{} for courseID, sectionID := range sections { @@ -59,5 +61,83 @@ func accessUser(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal }) } - return data + return +} + +func handleDeleteUser(dataSubjectId string, currentDbObjLocator pal.Locator, dbObj pal.DatabaseObject) (nodesToTraverse []pal.Locator, deleteNode bool, fieldsToUpdate pal.FieldUpdates, err error) { + deleteNode = true + + // add courses to nodesToTraverse + slice, ok := dbObj["courses"].([]interface{}) + if !ok { + err = cannotCastFieldToType("courses", "[]interface{}") + return + } + courses := make([]string, len(slice)) + for i, v := range slice { + course, ok := v.(string) + if !ok { + err = cannotCastFieldToType(fmt.Sprintf("courses element %v", v), "string") + return + } + courses[i] = course + } + for _, courseID := range courses { + nodesToTraverse = append(nodesToTraverse, pal.Locator{ + LocatorType: pal.Document, + DataType: CourseDataType, + FirestoreLocator: pal.FirestoreLocator{ + CollectionPath: []string{models.FirestoreCoursesCollection}, + DocIDs: []string{courseID}, + }, + }) + } + + // add defaultSections to nodesToTraverse + sections, ok := dbObj["defaultSections"].(map[string]interface{}) + if !ok { + err = cannotCastFieldToType("defaultSections", "map[string]interface{}") + return + } + for courseID, sectionID := range sections { + nodesToTraverse = append(nodesToTraverse, pal.Locator{ + LocatorType: pal.Document, + DataType: SectionDataType, + FirestoreLocator: pal.FirestoreLocator{ + CollectionPath: []string{models.FirestoreCoursesCollection, models.FirestoreSectionsCollection}, + DocIDs: []string{courseID, sectionID.(string)}, + }, + }) + } + + // add actualSections to nodesToTraverse + actualSections, ok := dbObj["actualSections"].(map[string]interface{}) + if !ok { + err = cannotCastFieldToType("actualSections", "map[string]interface{}") + return + } + for courseID, sectionsMap := range actualSections { + sections, ok := sectionsMap.(map[string]interface{}) + if !ok { + err = cannotCastFieldToType(fmt.Sprintf("actualSections entry %v", sectionsMap), "map[string]interface{}") + return + } + for _, sectionID := range sections { + id, ok := sectionID.(string) + if !ok { + err = cannotCastFieldToType(fmt.Sprintf("actualSections nested entry %v", sectionID), "string") + return + } + nodesToTraverse = append(nodesToTraverse, pal.Locator{ + LocatorType: pal.Document, + DataType: SectionDataType, + FirestoreLocator: pal.FirestoreLocator{ + CollectionPath: []string{models.FirestoreCoursesCollection, models.FirestoreSectionsCollection}, + DocIDs: []string{courseID, id}, + }, + }) + } + } + + return } diff --git a/backend/pkg/router/privacy.go b/backend/pkg/router/privacy.go index 1540f70..9c72eab 100644 --- a/backend/pkg/router/privacy.go +++ b/backend/pkg/router/privacy.go @@ -17,6 +17,7 @@ func PrivacyRoutes() *chi.Mux { router.Use(middleware.AuthCtx()) router.Get("/data", handleAccessRequest) + router.Get("/data/delete", handleDeleteRequest) return router } @@ -45,3 +46,29 @@ func handleAccessRequest(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") render.JSON(w, r, data) } + +func handleDeleteRequest(w http.ResponseWriter, r *http.Request) { + user, err := middleware.GetUserFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + locator := pal.Locator{ + LocatorType: pal.Document, + DataType: privacy.UserDataType, + FirestoreLocator: pal.FirestoreLocator{ + DocIDs: []string{user.ID}, + CollectionPath: []string{models.FirestoreProfilesCollection}, + }, + } + + res, err := repo.Repository.PrivacyPal.ProcessDeletionRequest(privacy.HandleDelete, locator, user.ID, true) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write([]byte(res)) + + // w.WriteHeader(http.StatusNoContent) +}