From 3a6292f675a2d1fd0b367d3fa1c6fcbc9b2afe6c Mon Sep 17 00:00:00 2001 From: Albin Antony Date: Sun, 15 Oct 2023 13:57:06 +0530 Subject: [PATCH] Add #244 Integrate policy endpoints Signed-off-by: George J Padayatti --- src/common/utils.go | 49 ++++ src/config/constants.go | 7 + src/database/db.go | 5 + .../addglobalpolicyconfigurations_handler.go | 135 +++++++++++ .../getglobalpolicyconfigurations_handler.go | 72 +++--- src/handlerv2/orgdeletepolicy_handler.go | 31 ++- src/handlerv2/orglistpolicy_handler.go | 129 +++++++++- .../orglistpolicyrevisions_handler.go | 63 ++++- ...teglobalpolicyconfigurationbyid_handler.go | 120 ++++++++- ...pdateglobalpolicyconfigurations_handler.go | 105 -------- src/httppathsv2/config_paths.go | 10 +- src/httppathsv2/routes.go | 2 +- src/paginate/paginate.go | 227 ++++++++++++++++++ src/policy/policy.go | 121 ++++++++++ src/policy/revision.go | 201 ++++++++++++++++ 15 files changed, 1114 insertions(+), 163 deletions(-) create mode 100644 src/handlerv2/addglobalpolicyconfigurations_handler.go delete mode 100644 src/handlerv2/updateglobalpolicyconfigurations_handler.go create mode 100644 src/paginate/paginate.go create mode 100644 src/policy/policy.go create mode 100644 src/policy/revision.go diff --git a/src/common/utils.go b/src/common/utils.go index 2040044..81036af 100644 --- a/src/common/utils.go +++ b/src/common/utils.go @@ -1,7 +1,10 @@ package common import ( + "crypto/sha1" + "encoding/hex" "encoding/json" + "fmt" "log" "math/rand" "net/http" @@ -119,6 +122,8 @@ func ParsePaginationQueryParameters(r *http.Request) (startID string, limit int) if ok { limit, _ = strconv.Atoi(limits[0]) + } else { + limit = 10 } return } @@ -172,3 +177,47 @@ func Sanitize(s string) string { p := bluemonday.UGCPolicy() return p.Sanitize(s) } + +func IntegerToSemver(version int) string { + major := version + minor := 0 + patch := 0 + + return fmt.Sprintf("%d.%d.%d", major, minor, patch) +} + +func BumpMajorVersion(version string) (string, error) { + // Split the version into major, minor, and patch + versionParts := strings.Split(version, ".") + + // Parse the parts into integers + major, err := strconv.Atoi(versionParts[0]) + if err != nil { + return version, err + } + + // Increment the major version and reset minor and patch to zero + return fmt.Sprintf("%d.0.0", major+1), err +} + +func CalculateSHA1(data string) (string, error) { + // Convert the JSON string to bytes + dataBytes := []byte(data) + + // Create a new SHA-1 hasher + sha1Hasher := sha1.New() + + // Write the data to the hasher + _, err := sha1Hasher.Write(dataBytes) + if err != nil { + return "", err + } + + // Get the hash sum + hashSum := sha1Hasher.Sum(nil) + + // Convert the hash sum to a hex string + hashHex := hex.EncodeToString(hashSum) + + return hashHex, err +} diff --git a/src/config/constants.go b/src/config/constants.go index 59d3d0d..4cf7982 100644 --- a/src/config/constants.go +++ b/src/config/constants.go @@ -24,3 +24,10 @@ const ( PolicyId = "policyId" DataAgreementRecordId = "dataAgreementRecordId" ) + +// Schemas +const ( + DataAgreement = "dataAgreement" + Policy = "policy" + DataAgreementRecord = "dataAgreementRecord" +) diff --git a/src/database/db.go b/src/database/db.go index f1a49f3..b7802bb 100644 --- a/src/database/db.go +++ b/src/database/db.go @@ -122,6 +122,11 @@ func Init(config *config.Configuration) error { return err } + err = initCollection("policies", []string{"id"}, true) + if err != nil { + return err + } + return nil } diff --git a/src/handlerv2/addglobalpolicyconfigurations_handler.go b/src/handlerv2/addglobalpolicyconfigurations_handler.go new file mode 100644 index 0000000..02f4961 --- /dev/null +++ b/src/handlerv2/addglobalpolicyconfigurations_handler.go @@ -0,0 +1,135 @@ +package handlerv2 + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/policy" + "github.com/bb-consent/api/src/token" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type RevisionForSnapshot struct { + policy.Revision + SerializedSnapshot string `json:"-"` + Id string `json:"-"` + SuccessorId string `json:"-"` + PredecessorHash string `json:"-"` + PredecessorSignature string `json:"-"` + SerializedHash string `json:"-"` +} + +type addPolicyReq struct { + Policy policy.Policy `json:"policy" valid:"required"` +} + +type addPolicyResp struct { + Policy policy.Policy `json:"policy"` + Revision interface{} `json:"revision"` +} + +func validateAddPolicyRequestBody(policyReq addPolicyReq) error { + // validating request payload + valid, err := govalidator.ValidateStruct(policyReq) + if err != nil { + return err + } + + if !valid { + return errors.New("invalid request payload") + } + + if strings.TrimSpace(policyReq.Policy.Name) == "" { + return errors.New("missing mandatory param - Name") + } + + if strings.TrimSpace(policyReq.Policy.Url) == "" { + return errors.New("missing mandatory param - Url") + + } + + return nil +} + +func updatePolicyFromAddPolicyRequestBody(requestBody addPolicyReq, newPolicy policy.Policy) policy.Policy { + newPolicy.Name = requestBody.Policy.Name + newPolicy.Url = requestBody.Policy.Url + newPolicy.Jurisdiction = requestBody.Policy.Jurisdiction + newPolicy.IndustrySector = requestBody.Policy.IndustrySector + newPolicy.DataRetentionPeriodDays = requestBody.Policy.DataRetentionPeriodDays + newPolicy.GeographicRestriction = requestBody.Policy.GeographicRestriction + newPolicy.StorageLocation = requestBody.Policy.StorageLocation + return newPolicy +} + +// AddGlobalPolicyConfiguration Handler to add global policy configuration +func AddGlobalPolicyConfiguration(w http.ResponseWriter, r *http.Request) { + // Current user + orgAdminId := token.GetUserID(r) + + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + // Request body + var policyReq addPolicyReq + b, _ := io.ReadAll(r.Body) + defer r.Body.Close() + json.Unmarshal(b, &policyReq) + + // Validate request body + err := validateAddPolicyRequestBody(policyReq) + if err != nil { + common.HandleErrorV2(w, http.StatusBadRequest, err.Error(), err) + return + } + + version := common.IntegerToSemver(1) + + // Initialise policy + var newPolicy policy.Policy + newPolicy.Id = primitive.NewObjectID() + // Update policy from request body + newPolicy = updatePolicyFromAddPolicyRequestBody(policyReq, newPolicy) + newPolicy.OrganisationId = organisationId + newPolicy.IsDeleted = false + newPolicy.Version = version + + // Create new revision + newRevision, err := policy.CreateRevisionForPolicy(newPolicy, orgAdminId) + if err != nil { + m := fmt.Sprintf("Failed to create revision for new policy: %v", newPolicy.Name) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + newPolicy.Revisions = append(newPolicy.Revisions, newRevision) + + // Save the policy to db + savedPolicy, err := policy.Add(newPolicy) + if err != nil { + m := fmt.Sprintf("Failed to create new policy: %v", newPolicy.Name) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + // Constructing the response + var resp addPolicyResp + resp.Policy = savedPolicy + + var revisionForHTTPResponse policy.RevisionForHTTPResponse + revisionForHTTPResponse.Init(newRevision) + resp.Revision = revisionForHTTPResponse + + response, _ := json.Marshal(resp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) + +} diff --git a/src/handlerv2/getglobalpolicyconfigurations_handler.go b/src/handlerv2/getglobalpolicyconfigurations_handler.go index ce4fb68..8b829d0 100644 --- a/src/handlerv2/getglobalpolicyconfigurations_handler.go +++ b/src/handlerv2/getglobalpolicyconfigurations_handler.go @@ -4,66 +4,54 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" - "github.com/bb-consent/api/src/org" - "github.com/bb-consent/api/src/orgtype" + "github.com/bb-consent/api/src/policy" + "github.com/gorilla/mux" ) -type globalPolicyConfigurationResp struct { - PolicyURL string - DataRetention org.DataRetention - Jurisdiction string - Disclosure string - Type orgtype.OrgType - Restriction string - Shared3PP bool +type getPolicyResp struct { + Policy policy.Policy `json:"policy"` + Revision interface{} `json:"revision"` } // GetGlobalPolicyConfiguration Handler to get global policy configurations func GetGlobalPolicyConfiguration(w http.ResponseWriter, r *http.Request) { - organizationID := r.Header.Get(config.OrganizationId) + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) - o, err := org.Get(organizationID) - if err != nil { - m := fmt.Sprintf("Failed to fetch organization: %v", organizationID) - common.HandleError(w, http.StatusInternalServerError, m, err) - return - } + policyId := mux.Vars(r)[config.PolicyId] - // Constructing the response - var resp globalPolicyConfigurationResp - - resp.PolicyURL = o.PolicyURL - resp.DataRetention = o.DataRetention + // Parse the URL query parameters + queryParams := r.URL.Query() + revisionId := queryParams.Get("revisionId") - if len(strings.TrimSpace(o.Jurisdiction)) == 0 { - resp.Jurisdiction = o.Location - o.Jurisdiction = o.Location - } else { - resp.Jurisdiction = o.Jurisdiction + p, err := policy.Get(policyId, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to fetch policy: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return } + var revision policy.Revision + if revisionId != "" { + for _, p := range p.Revisions { + if p.Id == revisionId { + revision = p + } + } - if len(strings.TrimSpace(o.Disclosure)) == 0 { - resp.Disclosure = "false" - o.Disclosure = "false" } else { - resp.Disclosure = o.Disclosure + revision = p.Revisions[len(p.Revisions)-1] } - resp.Type = o.Type - resp.Restriction = o.Restriction - resp.Shared3PP = o.Shared3PP + // Constructing the response + var resp getPolicyResp + resp.Policy = p - // Updating global configuration policy with defaults - _, err = org.Update(o) - if err != nil { - m := fmt.Sprintf("Failed to update global configuration with defaults to organization: %v", organizationID) - common.HandleError(w, http.StatusInternalServerError, m, err) - return - } + var revisionForHTTPResponse policy.RevisionForHTTPResponse + revisionForHTTPResponse.Init(revision) + resp.Revision = revisionForHTTPResponse response, _ := json.Marshal(resp) w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) diff --git a/src/handlerv2/orgdeletepolicy_handler.go b/src/handlerv2/orgdeletepolicy_handler.go index a8417b6..474608f 100644 --- a/src/handlerv2/orgdeletepolicy_handler.go +++ b/src/handlerv2/orgdeletepolicy_handler.go @@ -2,18 +2,43 @@ package handlerv2 import ( "encoding/json" + "fmt" "net/http" + "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/policy" + "github.com/gorilla/mux" ) // OrgDeletePolicy Handler to delete global policy revision func OrgDeletePolicy(w http.ResponseWriter, r *http.Request) { + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + policyId := mux.Vars(r)[config.PolicyId] - // Constructing the response - var resp globalPolicyConfigurationResp + currentPolicy, err := policy.Get(policyId, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to fetch policy: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } - response, _ := json.Marshal(resp) + currentRevision := currentPolicy.Revisions[len(currentPolicy.Revisions)-1] + + currentPolicy.IsDeleted = true + + _, err = policy.Update(currentPolicy, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to delete policy: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + var revisionForHTTPResponse policy.RevisionForHTTPResponse + revisionForHTTPResponse.Init(currentRevision) + + response, _ := json.Marshal(revisionForHTTPResponse) w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) w.WriteHeader(http.StatusOK) w.Write(response) diff --git a/src/handlerv2/orglistpolicy_handler.go b/src/handlerv2/orglistpolicy_handler.go index 5a89e65..a01febe 100644 --- a/src/handlerv2/orglistpolicy_handler.go +++ b/src/handlerv2/orglistpolicy_handler.go @@ -1,21 +1,142 @@ package handlerv2 import ( + "context" "encoding/json" + "errors" + "fmt" + "log" "net/http" + "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/paginate" + "github.com/bb-consent/api/src/policy" + "go.mongodb.org/mongo-driver/bson" ) -// OrgListPolicy Handler to list all global policies -func OrgListPolicy(w http.ResponseWriter, r *http.Request) { +// ListPoliciesError is an error enumeration for list policies API. +type ListPoliciesError int + +const ( + // ErrRevisionIDIsMissing indicates that the revisionId query param is missing. + RevisionIDIsMissingError ListPoliciesError = iota +) + +// Error returns the string representation of the error. +func (e ListPoliciesError) Error() string { + switch e { + case RevisionIDIsMissingError: + return "Query param revisionId is missing!" + default: + return "Unknown error!" + } +} - // Constructing the response - var resp globalPolicyConfigurationResp +// ParseListPoliciesQueryParams parses query params for listing policies. +func ParseListPoliciesQueryParams(r *http.Request) (revisionId string, err error) { + query := r.URL.Query() + // Check if revisionId query param is provided. + if r, ok := query["revisionId"]; ok && len(r) > 0 { + return r[0], nil + } + + return "", RevisionIDIsMissingError +} + +type listPoliciesResp struct { + Policies interface{} `json:"policies"` + Pagination paginate.Pagination `json:"pagination"` +} + +func returnHTTPResponse(resp interface{}, w http.ResponseWriter) { response, _ := json.Marshal(resp) w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) w.WriteHeader(http.StatusOK) w.Write(response) +} + +// OrgListPolicy Handler to list all global policies +func OrgListPolicy(w http.ResponseWriter, r *http.Request) { + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + var resp listPoliciesResp + + // Query params + offset, limit := paginate.ParsePaginationQueryParams(r) + log.Printf("Offset: %v and limit: %v\n", offset, limit) + revisionId, err := ParseListPoliciesQueryParams(r) + if err != nil && errors.Is(err, RevisionIDIsMissingError) { + // Return all policies + var policies []policy.Policy + query := paginate.PaginateDBObjectsQuery{ + Filter: bson.M{"organisationid": organisationId, "isdeleted": false}, + Collection: policy.Collection(), + Context: context.Background(), + Limit: limit, + Offset: offset, + } + result, err := paginate.PaginateDBObjects(query, &policies) + if err != nil { + if errors.Is(err, paginate.EmptyDBError) { + emptyPolicies := make([]interface{}, 0) + resp = listPoliciesResp{ + Policies: emptyPolicies, + Pagination: result.Pagination, + } + returnHTTPResponse(resp, w) + return + } + m := "Failed to paginate policy" + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + + } + resp = listPoliciesResp{ + Policies: result.Items, + Pagination: result.Pagination, + } + returnHTTPResponse(resp, w) + return + + } else { + // Fetch revision by id + revision, err := policy.GetRevisionById(revisionId, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to fetch policy by revision: %v", revisionId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + // Recreate policy from revision + p, err := policy.RecreatePolicyFromRevision(revision) + if err != nil { + m := fmt.Sprintf("Failed to fetch policy by revision: %v", revisionId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + interfaceSlice := make([]interface{}, 0) + interfaceSlice = append(interfaceSlice, p) + + // Constructing the response + resp = listPoliciesResp{ + Policies: interfaceSlice, + Pagination: paginate.Pagination{ + CurrentPage: 1, + TotalItems: 1, + TotalPages: 1, + Limit: 1, + HasPrevious: false, + HasNext: false, + }, + } + + } + + returnHTTPResponse(resp, w) } diff --git a/src/handlerv2/orglistpolicyrevisions_handler.go b/src/handlerv2/orglistpolicyrevisions_handler.go index 0c51375..eadc62c 100644 --- a/src/handlerv2/orglistpolicyrevisions_handler.go +++ b/src/handlerv2/orglistpolicyrevisions_handler.go @@ -2,16 +2,75 @@ package handlerv2 import ( "encoding/json" + "fmt" "net/http" + "strconv" + "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/paginate" + "github.com/bb-consent/api/src/policy" + "github.com/gorilla/mux" ) +type listRevisionsResp struct { + Policy policy.Policy `json:"policy"` + Revisions interface{} `json:"revisions"` + Pagination paginate.Pagination `json:"pagination"` +} + +func revisionsToInterfaceSlice(revisions []policy.RevisionForHTTPResponse) []interface{} { + interfaceSlice := make([]interface{}, len(revisions)) + for i, r := range revisions { + interfaceSlice[i] = r + } + return interfaceSlice +} + // OrgListPolicyRevisions Handler to list global policy revisions func OrgListPolicyRevisions(w http.ResponseWriter, r *http.Request) { - // Constructing the response - var resp globalPolicyConfigurationResp + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + policyId := mux.Vars(r)[config.PolicyId] + + p, err := policy.Get(policyId, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to fetch policy: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + // Limit - Total number of items in current page + limit, err := strconv.Atoi(r.URL.Query().Get("limit")) + if err != nil || limit < 0 { + limit = 10 + } + + // Offset - Total number of items to be skipped + offset, err := strconv.Atoi(r.URL.Query().Get("offset")) + if err != nil || offset < 0 { + offset = 0 + } + + query := paginate.PaginateObjectsQuery{ + Limit: limit, + Offset: offset, + } + + revisionForHTTPResponses := make([]policy.RevisionForHTTPResponse, len(p.Revisions)) + for i, r := range p.Revisions { + revisionForHTTPResponses[i].Init(r) + } + + interfaceSlice := revisionsToInterfaceSlice(revisionForHTTPResponses) + result := paginate.PaginateObjects(query, interfaceSlice) + + var resp = listRevisionsResp{ + Policy: p, + Revisions: result.Items, + Pagination: result.Pagination, + } response, _ := json.Marshal(resp) w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) diff --git a/src/handlerv2/updateglobalpolicyconfigurationbyid_handler.go b/src/handlerv2/updateglobalpolicyconfigurationbyid_handler.go index 1e6cbb5..fbb93b4 100644 --- a/src/handlerv2/updateglobalpolicyconfigurationbyid_handler.go +++ b/src/handlerv2/updateglobalpolicyconfigurationbyid_handler.go @@ -2,16 +2,134 @@ package handlerv2 import ( "encoding/json" + "errors" + "fmt" + "io" "net/http" + "strings" + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/policy" + "github.com/bb-consent/api/src/token" + "github.com/gorilla/mux" ) +type updatePolicyReq struct { + Policy policy.Policy `json:"policy" valid:"required"` +} + +type updatePolicyResp struct { + Policy policy.Policy `json:"policy"` + Revision interface{} `json:"revision"` +} + +func validateUpdatePolicyRequestBody(policyReq updatePolicyReq) error { + // validating request payload + valid, err := govalidator.ValidateStruct(policyReq) + if err != nil { + return err + } + + if !valid { + return errors.New("invalid request payload") + } + + if strings.TrimSpace(policyReq.Policy.Name) == "" { + return errors.New("missing mandatory param - Name") + } + + if strings.TrimSpace(policyReq.Policy.Url) == "" { + return errors.New("missing mandatory param - Url") + + } + + return nil +} + +func updatePolicyFromRequestBody(requestBody updatePolicyReq, toBeUpdatedPolicy policy.Policy) policy.Policy { + toBeUpdatedPolicy.Name = requestBody.Policy.Name + toBeUpdatedPolicy.Url = requestBody.Policy.Url + toBeUpdatedPolicy.Jurisdiction = requestBody.Policy.Jurisdiction + toBeUpdatedPolicy.IndustrySector = requestBody.Policy.IndustrySector + toBeUpdatedPolicy.DataRetentionPeriodDays = requestBody.Policy.DataRetentionPeriodDays + toBeUpdatedPolicy.GeographicRestriction = requestBody.Policy.GeographicRestriction + toBeUpdatedPolicy.StorageLocation = requestBody.Policy.StorageLocation + return toBeUpdatedPolicy +} + // UpdateGlobalPolicyConfigurationById Handler to update global policy configuration func UpdateGlobalPolicyConfigurationById(w http.ResponseWriter, r *http.Request) { + // Current user + orgAdminId := token.GetUserID(r) + + // Headers + organisationId := r.Header.Get(config.OrganizationId) + organisationId = common.Sanitize(organisationId) + + // Path params + policyId := mux.Vars(r)[config.PolicyId] + + // Request body + var policyReq updatePolicyReq + b, _ := io.ReadAll(r.Body) + defer r.Body.Close() + json.Unmarshal(b, &policyReq) + + // Validate request body + err := validateUpdatePolicyRequestBody(policyReq) + if err != nil { + common.HandleErrorV2(w, http.StatusBadRequest, err.Error(), err) + return + } + + // Get policy from db + toBeUpdatedPolicy, err := policy.Get(policyId, organisationId) + if err != nil { + common.HandleErrorV2(w, http.StatusInternalServerError, err.Error(), err) + return + } + + // Get current revision + currentRevision := toBeUpdatedPolicy.Revisions[len(toBeUpdatedPolicy.Revisions)-1] + + // Update policy from request body + toBeUpdatedPolicy = updatePolicyFromRequestBody(policyReq, toBeUpdatedPolicy) + + // Bump major version for policy + updatedVersion, err := common.BumpMajorVersion(toBeUpdatedPolicy.Version) + if err != nil { + m := fmt.Sprintf("Failed to bump major version for policy: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + toBeUpdatedPolicy.Version = updatedVersion + + // Update revision + newRevision, err := policy.UpdateRevisionForPolicy(toBeUpdatedPolicy, currentRevision, orgAdminId) + if err != nil { + m := fmt.Sprintf("Failed to update revision for policy: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + toBeUpdatedPolicy.Revisions = append(toBeUpdatedPolicy.Revisions, newRevision) + + // Save the policy to db + savedPolicy, err := policy.Update(toBeUpdatedPolicy, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to update policy: %v", toBeUpdatedPolicy.Name) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } // Constructing the response - var resp globalPolicyConfigurationResp + var resp updatePolicyResp + resp.Policy = savedPolicy + + var revisionForHTTPResponse policy.RevisionForHTTPResponse + revisionForHTTPResponse.Init(newRevision) + resp.Revision = revisionForHTTPResponse response, _ := json.Marshal(resp) w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) diff --git a/src/handlerv2/updateglobalpolicyconfigurations_handler.go b/src/handlerv2/updateglobalpolicyconfigurations_handler.go deleted file mode 100644 index a27ed1d..0000000 --- a/src/handlerv2/updateglobalpolicyconfigurations_handler.go +++ /dev/null @@ -1,105 +0,0 @@ -package handlerv2 - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - - "github.com/bb-consent/api/src/common" - "github.com/bb-consent/api/src/config" - "github.com/bb-consent/api/src/org" - "github.com/bb-consent/api/src/orgtype" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -type globalPolicyConfigurationReq struct { - PolicyURL string - RetentionPeriod int - Jurisdiction string - Disclosure string - TypeID string `valid:"required"` - Restriction string - Shared3PP bool -} - -// UpdateGlobalPolicyConfiguration Handler to update global policy configuration -func UpdateGlobalPolicyConfiguration(w http.ResponseWriter, r *http.Request) { - organizationID := r.Header.Get(config.OrganizationId) - - o, err := org.Get(organizationID) - if err != nil { - m := fmt.Sprintf("Failed to fetch organization: %v", organizationID) - common.HandleError(w, http.StatusInternalServerError, m, err) - return - } - - var policyReq globalPolicyConfigurationReq - b, _ := ioutil.ReadAll(r.Body) - defer r.Body.Close() - - json.Unmarshal(b, &policyReq) - - // Update global policy configuration for the org - o.PolicyURL = policyReq.PolicyURL - - if len(strings.TrimSpace(policyReq.Jurisdiction)) != 0 { - o.Jurisdiction = policyReq.Jurisdiction - } - - o.Restriction = policyReq.Restriction - o.Shared3PP = policyReq.Shared3PP - - if policyReq.Disclosure == "false" || policyReq.Disclosure == "true" { - o.Disclosure = policyReq.Disclosure - } - - // Check if type id is valid bson objectid hex - if !primitive.IsValidObjectID(policyReq.TypeID) { - m := fmt.Sprintf("Invalid organization type ID: %v", policyReq.TypeID) - common.HandleError(w, http.StatusBadRequest, m, err) - return - } - - orgType, err := orgtype.Get(policyReq.TypeID) - if err != nil { - m := fmt.Sprintf("Invalid organization type ID: %v", policyReq.TypeID) - common.HandleError(w, http.StatusBadRequest, m, err) - return - } - - o.Type = orgType - - if policyReq.RetentionPeriod > 0 { - o.DataRetention.RetentionPeriod = int64(policyReq.RetentionPeriod) - o.DataRetention.Enabled = true - } else { - o.DataRetention.RetentionPeriod = 0 - o.DataRetention.Enabled = false - } - - // Updating global configuration policy with defaults - o, err = org.Update(o) - if err != nil { - m := fmt.Sprintf("Failed to update global configuration to organization: %v", organizationID) - common.HandleError(w, http.StatusInternalServerError, m, err) - return - } - - // Constructing the response - var resp globalPolicyConfigurationResp - resp.PolicyURL = o.PolicyURL - resp.DataRetention = o.DataRetention - resp.Jurisdiction = o.Jurisdiction - resp.Disclosure = o.Disclosure - resp.Type = o.Type - resp.Restriction = o.Restriction - resp.Shared3PP = o.Shared3PP - - response, _ := json.Marshal(resp) - w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) - w.WriteHeader(http.StatusOK) - w.Write(response) - -} diff --git a/src/httppathsv2/config_paths.go b/src/httppathsv2/config_paths.go index 1cf5760..211df59 100644 --- a/src/httppathsv2/config_paths.go +++ b/src/httppathsv2/config_paths.go @@ -2,11 +2,11 @@ package httppathsv2 // Global policy configuration const UpdateGlobalPolicyConfigurations = "/v2/config/policy" -const GetGlobalPolicyConfigurations = "/v2/config/policy" -const UpdateGlobalPolicyConfigurationById = "/v2/config/policy/{policyId}/" -const OrgListPolicyRevisions = "/v2/config/policy/{policyId}/revisions/" -const OrgDeletePolicy = "/v2/config/policy/{policyId}/" -const OrgListPolicy = "/v2/config/policies/" +const GetGlobalPolicyConfigurations = "/v2/config/policy/{policyId}" +const UpdateGlobalPolicyConfigurationById = "/v2/config/policy/{policyId}" +const OrgListPolicyRevisions = "/v2/config/policy/{policyId}/revisions" +const OrgDeletePolicy = "/v2/config/policy/{policyId}" +const OrgListPolicy = "/v2/config/policies" // Data agreements const GetDataAgreementById = "/v2/config/data-agreement/{dataAgreementId}" diff --git a/src/httppathsv2/routes.go b/src/httppathsv2/routes.go index 1d40e3b..708e469 100644 --- a/src/httppathsv2/routes.go +++ b/src/httppathsv2/routes.go @@ -11,7 +11,7 @@ import ( func SetRoutes(r *mux.Router, e *casbin.Enforcer) { // Organization global policy configuration r.Handle(GetGlobalPolicyConfigurations, m.Chain(handler.GetGlobalPolicyConfiguration, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") - r.Handle(UpdateGlobalPolicyConfigurations, m.Chain(handler.UpdateGlobalPolicyConfiguration, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("POST") + r.Handle(UpdateGlobalPolicyConfigurations, m.Chain(handler.AddGlobalPolicyConfiguration, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("POST") r.Handle(UpdateGlobalPolicyConfigurationById, m.Chain(handler.UpdateGlobalPolicyConfigurationById, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("PUT") r.Handle(OrgListPolicyRevisions, m.Chain(handler.OrgListPolicyRevisions, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("GET") r.Handle(OrgDeletePolicy, m.Chain(handler.OrgDeletePolicy, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate(), m.AddContentType())).Methods("DELETE") diff --git a/src/paginate/paginate.go b/src/paginate/paginate.go new file mode 100644 index 0000000..75e45aa --- /dev/null +++ b/src/paginate/paginate.go @@ -0,0 +1,227 @@ +package paginate + +import ( + "context" + "errors" + "math" + "net/http" + "reflect" + "strconv" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Pagination +type Pagination struct { + CurrentPage int `json:"currentPage"` + TotalItems int `json:"totalItems"` + TotalPages int `json:"totalPages"` + Limit int `json:"limit"` + HasPrevious bool `json:"hasPrevious"` + HasNext bool `json:"hasNext"` +} + +// PaginateDBObjectsQuery +type PaginateDBObjectsQuery struct { + Filter bson.M + Collection *mongo.Collection + Context context.Context + Limit int + Offset int +} + +// PaginatedDBResult +type PaginatedDBResult struct { + Items interface{} `json:"items"` + Pagination Pagination `json:"pagination"` +} + +// PaginationError is an error enumeration for pagination +type PaginationError int + +const ( + // EmptyDBError indicates that database is empty. + EmptyDBError PaginationError = iota +) + +// Error returns the string representation of the error. +func (e PaginationError) Error() string { + switch e { + case EmptyDBError: + return "Database is empty!" + default: + return "Unknown error!" + } +} + +// PaginateDBObjects +func PaginateDBObjects(query PaginateDBObjectsQuery, resultSlice interface{}) (*PaginatedDBResult, error) { + + // Calculate total items + totalItems, err := query.Collection.CountDocuments(query.Context, query.Filter) + if err != nil { + return nil, err + } + + if totalItems == 0 { + return &PaginatedDBResult{}, EmptyDBError + } + + // Ensure offset is not negative and limit is positive + if query.Offset < 0 { + query.Offset = 0 + } + if query.Limit <= 0 { + query.Limit = 1 + } + + // Ensure offset is within bounds + if query.Offset >= int(totalItems) { + query.Offset = int(totalItems) - query.Limit + } + + // Calculate pages and selected page based on offset and limit + totalPages := int(math.Ceil(float64(totalItems) / float64(query.Limit))) + currentPage := (query.Offset / query.Limit) + 1 + + // Ensure currentPage is within bounds + if currentPage < 1 { + currentPage = 1 + } else if currentPage > totalPages { + currentPage = totalPages + } + + // Initialize pagination structure + pagination := Pagination{ + CurrentPage: currentPage, + TotalItems: int(totalItems), + Limit: query.Limit, + TotalPages: totalPages, + HasPrevious: currentPage > 1, + HasNext: currentPage < totalPages, + } + + // Query the database + opts := options.Find().SetSkip(int64(query.Offset)).SetLimit(int64(query.Limit)) + cursor, err := query.Collection.Find(query.Context, query.Filter, opts) + if err != nil { + return nil, err + } + defer cursor.Close(query.Context) + + // Decode items + sliceValue := reflect.ValueOf(resultSlice) + if sliceValue.Kind() != reflect.Ptr || sliceValue.Elem().Kind() != reflect.Slice { + return nil, errors.New("resultSlice must be a slice pointer") + } + sliceElem := sliceValue.Elem() + itemTyp := sliceElem.Type().Elem() + + for cursor.Next(query.Context) { + itemPtr := reflect.New(itemTyp).Interface() + if err := cursor.Decode(itemPtr); err != nil { + return nil, err + } + sliceElem = reflect.Append(sliceElem, reflect.ValueOf(itemPtr).Elem()) + } + if sliceElem.Len() == 0 { + sliceElem = reflect.MakeSlice(sliceElem.Type(), 0, 0) + } + + if err := cursor.Err(); err != nil { + return nil, err + } + + return &PaginatedDBResult{ + Items: sliceElem.Interface(), + Pagination: pagination, + }, nil +} + +// PaginateObjectsQuery +type PaginateObjectsQuery struct { + Limit int + Offset int +} + +// PaginatedResult +type PaginatedResult struct { + Items interface{} `json:"items"` + Pagination Pagination `json:"pagination"` +} + +func PaginateObjects(query PaginateObjectsQuery, toBeSortedItems []interface{}) *PaginatedResult { + totalItems := len(toBeSortedItems) + + // Ensure offset is not negative and limit is positive + if query.Offset < 0 { + query.Offset = 0 + } + if query.Limit <= 0 { + query.Limit = 1 + } + + // Ensure offset is within bounds + if query.Offset >= totalItems { + query.Offset = totalItems - query.Limit + } + + // Calculate pages and selected page based on offset and limit + totalPages := int(math.Ceil(float64(totalItems) / float64(query.Limit))) + currentPage := (query.Offset / query.Limit) + 1 + + // Ensure currentPage is within bounds + if currentPage < 1 { + currentPage = 1 + } else if currentPage > totalPages { + currentPage = totalPages + } + + endIdx := query.Offset + query.Limit + if endIdx > totalItems { + endIdx = totalItems + } + + paginatedItems := toBeSortedItems[query.Offset:endIdx] + + return &PaginatedResult{ + Items: paginatedItems, + Pagination: Pagination{ + CurrentPage: currentPage, + TotalItems: totalItems, + TotalPages: totalPages, + Limit: query.Limit, + HasPrevious: query.Offset > 0, + HasNext: endIdx < totalItems, + }, + } + +} + +const DEFAULT_LIMIT int = 10 +const DEFAULT_OFFSET int = 0 + +// ParsePaginationQueryParams parses offset and limit from query parameters. +// If they are not available or invalid it returns default values. +func ParsePaginationQueryParams(r *http.Request) (offset int, limit int) { + query := r.URL.Query() + offset, limit = DEFAULT_OFFSET, DEFAULT_LIMIT + + // Check if offset query param is provided and if it is a valid integer. + if o, ok := query["offset"]; ok && len(o) > 0 { + if oInt, err := strconv.Atoi(o[0]); err == nil && oInt >= 0 { + offset = oInt + } + } + + // Check if limit query param is provided and if it is a valid integer. + if l, ok := query["limit"]; ok && len(l) > 0 { + if lInt, err := strconv.Atoi(l[0]); err == nil && lInt > 0 { + limit = lInt + } + } + + return offset, limit +} diff --git a/src/policy/policy.go b/src/policy/policy.go new file mode 100644 index 0000000..5a87c9d --- /dev/null +++ b/src/policy/policy.go @@ -0,0 +1,121 @@ +package policy + +import ( + "context" + "errors" + + "github.com/bb-consent/api/src/database" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +func Collection() *mongo.Collection { + return database.DB.Client.Database(database.DB.Name).Collection("policies") +} + +type Policy struct { + Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" valid:"required"` + Version string `json:"version"` + Url string `json:"url" valid:"required"` + Jurisdiction string `json:"jurisdiction"` + IndustrySector string `json:"industrySector"` + DataRetentionPeriodDays int `json:"dataRetentionPeriod"` + GeographicRestriction string `json:"geographicRestriction"` + StorageLocation string `json:"storageLocation"` + OrganisationId string `json:"-"` + IsDeleted bool `json:"-"` + Revisions []Revision `json:"-"` +} + +// Add Adds the policy to the db +func Add(policy Policy) (Policy, error) { + + _, err := Collection().InsertOne(context.TODO(), policy) + if err != nil { + return Policy{}, err + } + + return policy, nil +} + +// Get Gets a single policy by given id +func Get(policyID string, organisationId string) (Policy, error) { + policyId, err := primitive.ObjectIDFromHex(policyID) + if err != nil { + return Policy{}, err + } + + var result Policy + err = Collection().FindOne(context.TODO(), bson.M{"_id": policyId, "organisationid": organisationId, "isdeleted": false}).Decode(&result) + + return result, err +} + +// GetRevisionById Get revision by id +func GetRevisionById(revisionId string, organisationId string) (Revision, error) { + // Find the policy + matchStage := bson.M{ + "$match": bson.M{ + "organisationid": organisationId, + "isdeleted": false, + "revisions": bson.M{ + "$elemMatch": bson.M{ + "id": revisionId, + }, + }, + }, + } + // Select matched revision + projectStage := bson.M{ + "$project": bson.M{ // Ensure to use '$project' + "revisions": bson.M{ + "$filter": bson.M{ + "input": "$revisions", + "as": "r", + "cond": bson.M{ + "$eq": bson.A{"$$r.id", revisionId}, + }, + }, + }, + }, + } + pipeline := []bson.M{ + matchStage, + projectStage, + } + + // Execute aggregate pipeline + cur, err := Collection().Aggregate(context.TODO(), pipeline) + if err != nil { + return Revision{}, err + } + // Close the cursor + defer cur.Close(context.TODO()) + + // Deserialise aggregate results to policies + var policies []Policy + if err = cur.All(context.TODO(), &policies); err != nil { + return Revision{}, err + } + + if len(policies[0].Revisions) == 0 { + return Revision{}, errors.New("matching revision was not found") + } + + return policies[0].Revisions[0], err +} + +// Update Updates the policy +func Update(policy Policy, organisationId string) (Policy, error) { + + filter := bson.M{"_id": policy.Id, "organisationid": organisationId} + update := bson.M{"$set": policy} + + _, err := Collection().UpdateOne(context.TODO(), filter, update) + if err != nil { + return policy, err + } + return policy, err +} diff --git a/src/policy/revision.go b/src/policy/revision.go new file mode 100644 index 0000000..3a24fe4 --- /dev/null +++ b/src/policy/revision.go @@ -0,0 +1,201 @@ +package policy + +import ( + "encoding/json" + "time" + + "github.com/bb-consent/api/src/common" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Revision +type Revision struct { + Id string `json:"id"` + SchemaName string `json:"schemaName"` + ObjectId string `json:"objectId"` + SignedWithoutObjectId bool `json:"signedWithoutObjectId"` + Timestamp string `json:"timestamp"` + AuthorizedByIndividualId string `json:"authorizedByIndividualId"` + AuthorizedByOtherId string `json:"authorizedByOtherId"` + PredecessorHash string `json:"predecessorHash"` + PredecessorSignature string `json:"predecessorSignature"` + ObjectData string `json:"objectData"` + SuccessorId string `json:"-"` + SerializedHash string `json:"-"` + SerializedSnapshot string `json:"-"` +} + +// Init +func (r *Revision) Init(objectId string, authorisedByOtherId string) { + r.Id = primitive.NewObjectID().Hex() + r.SchemaName = "policy" + r.ObjectId = objectId + r.SignedWithoutObjectId = false + r.Timestamp = time.Now().UTC().Format("2006-01-02T15:04:05Z") + r.AuthorizedByIndividualId = "" + r.AuthorizedByOtherId = authorisedByOtherId +} + +func (r *Revision) updateSuccessorId(successorId string) { + r.SuccessorId = successorId +} + +func (r *Revision) updatePredecessorSignature(signature string) { + r.PredecessorSignature = signature +} + +func (r *Revision) updatePredecessorHash(hash string) { + r.PredecessorHash = hash +} + +// CreateRevision +func (r *Revision) CreateRevision(objectData interface{}) error { + + // Object data + objectDataSerialised, err := json.Marshal(objectData) + if err != nil { + return err + } + r.ObjectData = string(objectDataSerialised) + + // Serialised snapshot + // TODO: Use a standard json normalisation algorithm for .e.g JCS + serialisedSnapshot, err := json.Marshal(r) + if err != nil { + return err + } + r.SerializedSnapshot = string(serialisedSnapshot) + + // Serialised hash using SHA-1 + r.SerializedHash, err = common.CalculateSHA1(string(serialisedSnapshot)) + if err != nil { + return err + } + + return nil + +} + +// UpdateRevision +func (r *Revision) UpdateRevision(previousRevision Revision, objectData interface{}) error { + // Update successor for previous revision + previousRevision.updateSuccessorId(r.Id) + + // Predecessor hash + r.updatePredecessorHash(previousRevision.PredecessorHash) + + // Predecessor signature + // TODO: Add signature for predecessor hash + signature := "" + r.updatePredecessorSignature(signature) + + // Create revision + err := r.CreateRevision(objectData) + if err != nil { + return err + } + + return nil +} + +type policyForObjectData struct { + Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" valid:"required"` + Version string `json:"version"` + Url string `json:"url" valid:"required"` + Jurisdiction string `json:"jurisdiction"` + IndustrySector string `json:"industrySector"` + DataRetentionPeriodDays int `json:"dataRetentionPeriod"` + GeographicRestriction string `json:"geographicRestriction"` + StorageLocation string `json:"storageLocation"` +} + +// CreateRevisionForPolicy +func CreateRevisionForPolicy(newPolicy Policy, orgAdminId string) (Revision, error) { + // Object data + objectData := policyForObjectData{ + Id: newPolicy.Id, + Name: newPolicy.Name, + Version: newPolicy.Version, + Url: newPolicy.Url, + Jurisdiction: newPolicy.Jurisdiction, + IndustrySector: newPolicy.IndustrySector, + DataRetentionPeriodDays: newPolicy.DataRetentionPeriodDays, + GeographicRestriction: newPolicy.GeographicRestriction, + StorageLocation: newPolicy.StorageLocation, + } + + // Create revision + revision := Revision{} + revision.Init(objectData.Id.Hex(), orgAdminId) + err := revision.CreateRevision(objectData) + + return revision, err +} + +// UpdateRevisionForPolicy +func UpdateRevisionForPolicy(updatedPolicy Policy, previousRevision Revision, orgAdminId string) (Revision, error) { + // Object data + objectData := policyForObjectData{ + Id: updatedPolicy.Id, + Name: updatedPolicy.Name, + Version: updatedPolicy.Version, + Url: updatedPolicy.Url, + Jurisdiction: updatedPolicy.Jurisdiction, + IndustrySector: updatedPolicy.IndustrySector, + DataRetentionPeriodDays: updatedPolicy.DataRetentionPeriodDays, + GeographicRestriction: updatedPolicy.GeographicRestriction, + StorageLocation: updatedPolicy.StorageLocation, + } + + // Update revision + revision := Revision{} + revision.Init(objectData.Id.Hex(), orgAdminId) + err := revision.UpdateRevision(previousRevision, objectData) + + return revision, err +} + +func RecreatePolicyFromRevision(revision Revision) (Policy, error) { + + // Deserialise revision snapshot + var r Revision + err := json.Unmarshal([]byte(revision.SerializedSnapshot), &r) + if err != nil { + return Policy{}, err + } + + // Deserialise policy + var p Policy + err = json.Unmarshal([]byte(r.ObjectData), &p) + if err != nil { + return Policy{}, err + } + + return p, nil +} + +// RevisionForHTTPResponse +type RevisionForHTTPResponse struct { + Revision + SuccessorId string `json:"successorId"` + SerializedHash string `json:"serializedHash"` + SerializedSnapshot string `json:"serizalizedSnapshot"` +} + +// Init +func (r *RevisionForHTTPResponse) Init(revision Revision) { + r.Id = revision.Id + r.SchemaName = revision.SchemaName + r.ObjectId = revision.ObjectId + r.SignedWithoutObjectId = revision.SignedWithoutObjectId + r.Timestamp = revision.Timestamp + r.AuthorizedByIndividualId = revision.AuthorizedByIndividualId + r.AuthorizedByOtherId = revision.AuthorizedByOtherId + r.PredecessorHash = revision.PredecessorHash + r.PredecessorSignature = revision.PredecessorSignature + r.ObjectData = revision.ObjectData + r.SuccessorId = revision.SuccessorId + r.SerializedHash = revision.SerializedHash + r.SerializedSnapshot = revision.SerializedSnapshot +}