From 04d84d46e66204ed74d9a6349e47ab54116dd43e 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 --- src/common/utils.go | 142 +++++++++++ src/config/constants.go | 7 + src/database/db.go | 5 + .../addglobalpolicyconfigurations_handler.go | 225 ++++++++++++++++++ .../getglobalpolicyconfigurations_handler.go | 69 ++---- src/handlerv2/orgdeletepolicy_handler.go | 27 ++- src/handlerv2/orglistpolicy_handler.go | 98 +++++++- .../orglistpolicyrevisions_handler.go | 64 ++++- ...teglobalpolicyconfigurationbyid_handler.go | 122 +++++++++- ...pdateglobalpolicyconfigurations_handler.go | 105 -------- src/httppathsv2/config_paths.go | 10 +- src/httppathsv2/routes.go | 2 +- src/policy/policy.go | 136 +++++++++++ 13 files changed, 849 insertions(+), 163 deletions(-) create mode 100644 src/handlerv2/addglobalpolicyconfigurations_handler.go delete mode 100644 src/handlerv2/updateglobalpolicyconfigurations_handler.go create mode 100644 src/policy/policy.go diff --git a/src/common/utils.go b/src/common/utils.go index 2040044..025fa9c 100644 --- a/src/common/utils.go +++ b/src/common/utils.go @@ -1,17 +1,26 @@ package common import ( + "context" + "crypto/sha1" + "encoding/hex" "encoding/json" + "errors" + "fmt" "log" "math/rand" "net/http" "path/filepath" + "reflect" "runtime" "strconv" "strings" "time" "github.com/microcosm-cc/bluemonday" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) const ( @@ -119,6 +128,8 @@ func ParsePaginationQueryParameters(r *http.Request) (startID string, limit int) if ok { limit, _ = strconv.Atoi(limits[0]) + } else { + limit = 10 } return } @@ -172,3 +183,134 @@ func Sanitize(s string) string { p := bluemonday.UGCPolicy() return p.Sanitize(s) } + +func SemverVersion(version int) string { + major := version + minor := 0 + patch := 0 + + return fmt.Sprintf("%d.%d.%d", major, minor, patch) +} + +func UpdateSemverVersion(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 +} + +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"` +} + +type PaginationQuery struct { + Filter bson.M + Collection *mongo.Collection + Context context.Context + CurrentPage int + Limit int + Offset int + Count int +} + +type PaginatedResult struct { + Items interface{} `json:"items"` + Pagination Pagination `json:"pagination"` +} + +func Paginate(query PaginationQuery, resultSlice interface{}) (*PaginatedResult, error) { + + // Calculate total items + opts := options.Count().SetSkip(int64(query.Offset)) + totalItems, err := query.Collection.CountDocuments(query.Context, query.Filter, opts) + if err != nil { + return nil, err + } + + // Initialize pagination structure + pagination := Pagination{ + CurrentPage: query.CurrentPage, + TotalItems: int(totalItems), + Limit: query.Limit, + } + + // Calculate total pages + pagination.TotalPages = int(totalItems) / query.Limit + if int(totalItems)%query.Limit > 0 { + pagination.TotalPages++ + } + + // Set HasNext and HasPrevious + pagination.HasPrevious = query.CurrentPage > 1 + pagination.HasNext = query.CurrentPage < pagination.TotalPages + + // Query the database + findOpts := options.Find().SetSkip(int64(query.Offset + ((query.CurrentPage - 1) * query.Limit))).SetLimit(int64(query.Limit)) + cursor, err := query.Collection.Find(query.Context, query.Filter, findOpts) + 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 &PaginatedResult{ + Items: sliceElem.Interface(), + Pagination: pagination, + }, nil +} 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..47b3038 --- /dev/null +++ b/src/handlerv2/addglobalpolicyconfigurations_handler.go @@ -0,0 +1,225 @@ +package handlerv2 + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + "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 RevisionWithoutSnapshot 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"` +} + +type PolicyWithoutSnapshot 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:"organisationId"` + IsDeleted bool `json:"isDeleted"` + Revisions []RevisionWithoutSnapshot `json:"revision"` +} + +type PolicyReq struct { + Id string `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"` +} +type addPolicyReq struct { + Policy PolicyReq `json:"policy" valid:"required"` +} +type PolicyResp struct { + Id string `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"` +} + +type PolicyRespWithRevision struct { + Policy PolicyResp `json:"policy"` + Revisions policy.Revision `json:"revision"` +} + +// AddGlobalPolicyConfiguration Handler to add global policy configuration +func AddGlobalPolicyConfiguration(w http.ResponseWriter, r *http.Request) { + organizationID := r.Header.Get(config.OrganizationId) + + userID := token.GetUserID(r) + + var policyReq addPolicyReq + b, _ := ioutil.ReadAll(r.Body) + defer r.Body.Close() + + json.Unmarshal(b, &policyReq) + + // validating request payload + valid, err := govalidator.ValidateStruct(policyReq) + if !valid { + log.Printf("Missing mandatory params for adding policy") + common.HandleErrorV2(w, http.StatusBadRequest, err.Error(), err) + return + } + + // checking if the string contained whitespace only + if strings.TrimSpace(policyReq.Policy.Name) == "" { + m := "Failed to add policy: Missing mandatory param - Name" + common.HandleErrorV2(w, http.StatusBadRequest, m, errors.New("missing mandatory param - Name")) + return + } + + if strings.TrimSpace(policyReq.Policy.Url) == "" { + m := "Failed to add policy: Missing mandatory param - Url" + common.HandleErrorV2(w, http.StatusBadRequest, m, errors.New("missing mandatory param - Name")) + return + } + semvarVersion := common.SemverVersion(1) + + var p policy.Policy + p.Id = primitive.NewObjectID() + p.Name = policyReq.Policy.Name + p.Version = semvarVersion + p.Url = policyReq.Policy.Url + p.Jurisdiction = policyReq.Policy.Jurisdiction + p.IndustrySector = policyReq.Policy.IndustrySector + p.DataRetentionPeriodDays = policyReq.Policy.DataRetentionPeriodDays + p.GeographicRestriction = policyReq.Policy.GeographicRestriction + p.StorageLocation = policyReq.Policy.StorageLocation + p.OrganisationId = organizationID + p.IsDeleted = false + + var revision policy.Revision + + timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z") + + revision.Id = primitive.NewObjectID().Hex() + revision.SchemaName = config.Policy + revision.ObjectId = p.Id.Hex() + revision.SignedWithoutObjectId = false + revision.Timestamp = timestamp + revision.AuthorizedByIndividualId = "" + revision.AuthorizedByOtherId = userID + + serializedSnapshot, err := CreatePolicySerializedSnapshot(p, revision) + if err != nil { + m := "Failed to create policy serialized snapshot" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + + serializedHash, err := common.CalculateSHA1(serializedSnapshot) + if err != nil { + m := "Failed to create policy serialized hash" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + + revision.SerializedSnapshot = serializedSnapshot + revision.SerializedHash = serializedHash + + p.Revisions = append(p.Revisions, revision) + + policyResp, err := policy.Add(p) + if err != nil { + m := fmt.Sprintf("Failed to add policy: %v", p.Name) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + addPolicyResp := createPolicyResp(policyResp) + + // Constructing the response + var resp PolicyRespWithRevision + resp.Policy = addPolicyResp + resp.Revisions = policyResp.Revisions[0] + + response, _ := json.Marshal(resp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) + +} + +func CreatePolicySerializedSnapshot(policy policy.Policy, revision policy.Revision) (string, error) { + var policyReq PolicyWithoutSnapshot + + policyReq.Id = policy.Id + policyReq.Name = policy.Name + policyReq.Version = policy.Version + policyReq.Url = policy.Url + policyReq.Jurisdiction = policy.Jurisdiction + policyReq.IndustrySector = policy.IndustrySector + policyReq.DataRetentionPeriodDays = policy.DataRetentionPeriodDays + policyReq.GeographicRestriction = policy.GeographicRestriction + policyReq.StorageLocation = policy.StorageLocation + + var revisionReq RevisionWithoutSnapshot + + revisionReq.Id = revision.Id + revisionReq.SchemaName = revision.SchemaName + revisionReq.ObjectId = revision.ObjectId + revisionReq.SignedWithoutObjectId = revision.SignedWithoutObjectId + revisionReq.Timestamp = revision.Timestamp + revisionReq.AuthorizedByIndividualId = revision.AuthorizedByIndividualId + revisionReq.AuthorizedByOtherId = revision.AuthorizedByOtherId + + policyReq.Revisions = append(policyReq.Revisions, revisionReq) + + snapshotByte, err := json.Marshal(policyReq) + if err != nil { + log.Println("Failed to create policy serialized snapshot") + return "", err + } + + serializedSnapshot := string(snapshotByte) + + return serializedSnapshot, err + +} + +func createPolicyResp(p policy.Policy) PolicyResp { + var policyResp PolicyResp + policyResp.Id = p.Id.Hex() + policyResp.Name = p.Name + policyResp.Version = p.Version + policyResp.Url = p.Url + policyResp.Jurisdiction = p.Jurisdiction + policyResp.IndustrySector = p.IndustrySector + policyResp.DataRetentionPeriodDays = p.DataRetentionPeriodDays + policyResp.GeographicRestriction = p.GeographicRestriction + policyResp.StorageLocation = p.StorageLocation + + return policyResp +} diff --git a/src/handlerv2/getglobalpolicyconfigurations_handler.go b/src/handlerv2/getglobalpolicyconfigurations_handler.go index ce4fb68..cf59591 100644 --- a/src/handlerv2/getglobalpolicyconfigurations_handler.go +++ b/src/handlerv2/getglobalpolicyconfigurations_handler.go @@ -4,66 +4,47 @@ 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 -} - // 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) - 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 - } - - // Constructing the response - var resp globalPolicyConfigurationResp + policyId := mux.Vars(r)[config.PolicyId] - 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 + policyResp, 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 policyResp.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 = policyResp.Revisions[len(policyResp.Revisions)-1] } - resp.Type = o.Type - resp.Restriction = o.Restriction - resp.Shared3PP = o.Shared3PP + getPolicyResp := createPolicyResp(policyResp) - // 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 - } + // Constructing the response + var resp PolicyRespWithRevision + resp.Policy = getPolicyResp + resp.Revisions = revision 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..3b769d3 100644 --- a/src/handlerv2/orgdeletepolicy_handler.go +++ b/src/handlerv2/orgdeletepolicy_handler.go @@ -2,18 +2,39 @@ 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) + 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 + } + + response, _ := json.Marshal(currentRevision) 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..c83a1dc 100644 --- a/src/handlerv2/orglistpolicy_handler.go +++ b/src/handlerv2/orglistpolicy_handler.go @@ -1,17 +1,92 @@ package handlerv2 import ( + "context" "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/policy" + "go.mongodb.org/mongo-driver/bson" ) +type PaginatedResult struct { + Items interface{} `json:"policies"` + Pagination common.Pagination `json:"pagination"` +} + // OrgListPolicy Handler to list all global policies func OrgListPolicy(w http.ResponseWriter, r *http.Request) { + organisationId := r.Header.Get(config.OrganizationId) + + // Parse the URL query parameters + queryParams := r.URL.Query() + revisionId := queryParams.Get("revisionId") + + if revisionId != "" { + policyResp, err := policy.GetByRevisionId(revisionId, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to fetch policy by revision: %v", revisionId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + listPolicyResp := createPolicyResp(policyResp) + // Constructing the response + var resp = &PaginatedResult{ + Items: listPolicyResp, + Pagination: common.Pagination{ + CurrentPage: 1, + TotalItems: 1, + TotalPages: 1, + Limit: 1, + HasPrevious: false, + HasNext: false, + }, + } + + response, _ := json.Marshal(resp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) + return + } + _, limit := common.ParsePaginationQueryParameters(r) + + // Offset - Number of documents skipped in the query + offset, err := strconv.Atoi(r.URL.Query().Get("offset")) + if err != nil || offset < 0 { + offset = 0 + } - // Constructing the response - var resp globalPolicyConfigurationResp + // Page - current page + page, err := strconv.Atoi(r.URL.Query().Get("page")) + if err != nil || page <= 0 { + page = 1 + } + + var policies []PolicyResp + query := common.PaginationQuery{ + Filter: bson.M{"organisationid": organisationId, "isdeleted": false}, + Collection: policy.Collection(), + Context: context.Background(), + CurrentPage: page, + Limit: limit, + Offset: offset, + } + result, err := common.Paginate(query, &policies) + if err != nil { + m := "Failed to paginate policy" + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + var resp = &PaginatedResult{ + Items: result.Items, + Pagination: result.Pagination, + } response, _ := json.Marshal(resp) w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) @@ -19,3 +94,22 @@ func OrgListPolicy(w http.ResponseWriter, r *http.Request) { w.Write(response) } + +func CreatePolicyResp(policyReq []policy.Policy) []PolicyResp { + var pResp []PolicyResp + var policyResp PolicyResp + + for _, p := range policyReq { + policyResp.Id = p.Id.Hex() + policyResp.Name = p.Name + policyResp.Version = p.Version + policyResp.Url = p.Url + policyResp.Jurisdiction = p.Jurisdiction + policyResp.IndustrySector = p.IndustrySector + policyResp.DataRetentionPeriodDays = p.DataRetentionPeriodDays + policyResp.GeographicRestriction = p.GeographicRestriction + policyResp.StorageLocation = p.StorageLocation + pResp = append(pResp, policyResp) + } + return pResp +} diff --git a/src/handlerv2/orglistpolicyrevisions_handler.go b/src/handlerv2/orglistpolicyrevisions_handler.go index 0c51375..d9d19ad 100644 --- a/src/handlerv2/orglistpolicyrevisions_handler.go +++ b/src/handlerv2/orglistpolicyrevisions_handler.go @@ -1,17 +1,77 @@ package handlerv2 import ( + "context" "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/policy" + "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/bson" ) +type ListPolicyRevisionsResp struct { + Policy PolicyResp `json:"policy"` + Items interface{} `json:"revisions"` + Pagination common.Pagination `json:"pagination"` +} + // 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) + policyId := mux.Vars(r)[config.PolicyId] + + policyResp, 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 := common.ParsePaginationQueryParameters(r) + + // Offset - Number of documents skipped in the query + offset, err := strconv.Atoi(r.URL.Query().Get("offset")) + if err != nil || offset < 0 { + offset = 0 + } + + // Page - current page + page, err := strconv.Atoi(r.URL.Query().Get("page")) + if err != nil || page <= 0 { + page = 1 + } + + revisionCount := len(policyResp.Revisions[offset:]) + query := common.PaginationQuery{ + Filter: bson.M{"_id": policyResp.Id, "organisationid": organisationId, "isdeleted": false}, + Collection: policy.Collection(), + Context: context.Background(), + CurrentPage: page, + Limit: limit, + Offset: offset, + Count: revisionCount, + } + + result, err := policy.PaginateRevisonsField(query) + if err != nil { + m := fmt.Sprintf("Failed to paginate policy revisions: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + listPolicyResp := createPolicyResp(policyResp) + + var resp = ListPolicyRevisionsResp{ + Policy: listPolicyResp, + Items: 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..c4c4070 100644 --- a/src/handlerv2/updateglobalpolicyconfigurationbyid_handler.go +++ b/src/handlerv2/updateglobalpolicyconfigurationbyid_handler.go @@ -2,16 +2,136 @@ package handlerv2 import ( "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" "net/http" + "strings" + "time" + "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" + "go.mongodb.org/mongo-driver/bson/primitive" ) +type updatePolicyReq struct { + Policy PolicyReq `json:"policy" valid:"required"` +} + // UpdateGlobalPolicyConfigurationById Handler to update global policy configuration func UpdateGlobalPolicyConfigurationById(w http.ResponseWriter, r *http.Request) { + organisationId := r.Header.Get(config.OrganizationId) + policyId := mux.Vars(r)[config.PolicyId] + userID := token.GetUserID(r) + + var policyReq updatePolicyReq + b, _ := ioutil.ReadAll(r.Body) + defer r.Body.Close() + + json.Unmarshal(b, &policyReq) + + // validating request payload + valid, err := govalidator.ValidateStruct(policyReq) + if !valid { + log.Printf("Missing mandatory params for adding policy") + common.HandleErrorV2(w, http.StatusBadRequest, err.Error(), err) + return + } + + // checking if the string contained whitespace only + if strings.TrimSpace(policyReq.Policy.Name) == "" { + m := "Failed to update policy: Missing mandatory param - Name" + common.HandleErrorV2(w, http.StatusBadRequest, m, errors.New("missing mandatory param - Name")) + return + } + + if strings.TrimSpace(policyReq.Policy.Url) == "" { + m := "Failed to update policy: Missing mandatory param - Url" + common.HandleErrorV2(w, http.StatusBadRequest, m, errors.New("missing mandatory param - Name")) + return + } + + 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 + } + + currentRevision := currentPolicy.Revisions[len(currentPolicy.Revisions)-1] + + version, err := common.UpdateSemverVersion(currentPolicy.Version) + if err != nil { + m := fmt.Sprintf("Failed to updtae policy version: %v", policyId) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + currentPolicy.Name = policyReq.Policy.Name + currentPolicy.Version = version + currentPolicy.Url = policyReq.Policy.Url + currentPolicy.Jurisdiction = policyReq.Policy.Jurisdiction + currentPolicy.IndustrySector = policyReq.Policy.IndustrySector + currentPolicy.DataRetentionPeriodDays = policyReq.Policy.DataRetentionPeriodDays + currentPolicy.GeographicRestriction = policyReq.Policy.GeographicRestriction + currentPolicy.StorageLocation = policyReq.Policy.StorageLocation + + var revision policy.Revision + + timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z") + + revision.Id = primitive.NewObjectID().Hex() + revision.SchemaName = config.Policy + revision.ObjectId = currentPolicy.Id.Hex() + revision.SignedWithoutObjectId = false + revision.Timestamp = timestamp + revision.AuthorizedByIndividualId = "" + revision.AuthorizedByOtherId = userID + + serializedSnapshot, err := CreatePolicySerializedSnapshot(currentPolicy, revision) + if err != nil { + m := "Failed to create policy serialized snapshot" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + + serializedHash, err := common.CalculateSHA1(serializedSnapshot) + if err != nil { + m := "Failed to create policy serialized hash" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + + revision.SerializedSnapshot = serializedSnapshot + revision.SerializedHash = serializedHash + revision.PredecessorHash = currentRevision.SerializedHash + + currentRevision.SuccessorId = revision.Id + + previousRevisions := currentPolicy.Revisions[:len(currentPolicy.Revisions)-1] + + UpdatedRevisions := append(previousRevisions, currentRevision, revision) + + currentPolicy.Revisions = UpdatedRevisions + + policyResp, err := policy.Update(currentPolicy, organisationId) + if err != nil { + m := fmt.Sprintf("Failed to update policy: %v", currentPolicy.Name) + common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + return + } + + updatePolicyResp := createPolicyResp(policyResp) // Constructing the response - var resp globalPolicyConfigurationResp + var resp PolicyRespWithRevision + resp.Policy = updatePolicyResp + resp.Revisions = policyResp.Revisions[len(policyResp.Revisions)-1] 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/policy/policy.go b/src/policy/policy.go new file mode 100644 index 0000000..32ff5b0 --- /dev/null +++ b/src/policy/policy.go @@ -0,0 +1,136 @@ +package policy + +import ( + "context" + + "github.com/bb-consent/api/src/common" + "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" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Collection() *mongo.Collection { + return database.DB.Client.Database(database.DB.Name).Collection("policies") +} + +type Revision struct { + Id string `json:"id"` + SchemaName string `json:"schemaName"` + ObjectId string `json:"objectId"` + SignedWithoutObjectId bool `json:"signedWithoutObjectId"` + SerializedSnapshot string `json:"serializedSnapshot"` + SerializedHash string `json:"serializedHash"` + Timestamp string `json:"timestamp"` + AuthorizedByIndividualId string `json:"authorizedByIndividualId"` + AuthorizedByOtherId string `json:"authorizedByOtherId"` + SuccessorId string `json:"successorId"` + PredecessorHash string `json:"predecessorHash"` + PredecessorSignature string `json:"predecessorSignature"` +} + +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:"organisationId"` + IsDeleted bool `json:"isDeleted"` + Revisions []Revision `json:"revision"` +} + +// 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 +} + +// Get Gets a single policy by given id +func GetByRevisionId(revisionId string, organisationId string) (Policy, error) { + + var result Policy + filter := bson.M{ + "organisationid": organisationId, + "isdeleted": false, + "revisions": bson.M{ + "$elemMatch": bson.M{ + "id": revisionId, + }, + }, + } + err := Collection().FindOne(context.TODO(), filter).Decode(&result) + + return result, 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 +} + +func PaginateRevisonsField(query common.PaginationQuery) (*common.PaginatedResult, error) { + totalItems := query.Count + + // Initialize pagination structure + pagination := common.Pagination{ + CurrentPage: query.CurrentPage, + TotalItems: int(totalItems), + Limit: query.Limit, + } + + // Calculate total pages + pagination.TotalPages = int(totalItems) / query.Limit + if int(totalItems)%query.Limit > 0 { + pagination.TotalPages++ + } + + // Set HasNext and HasPrevious + pagination.HasPrevious = query.CurrentPage > 1 + pagination.HasNext = query.CurrentPage < pagination.TotalPages + + var result Policy + opts := options.FindOne() + opts.SetProjection(bson.M{"revisions": bson.M{"$slice": []interface{}{int64(query.Offset + ((query.CurrentPage - 1) * query.Limit)), query.Limit}}}) + err := query.Collection.FindOne(query.Context, query.Filter, opts).Decode(&result) + if err != nil { + return nil, err + } + + return &common.PaginatedResult{ + Items: result.Revisions, + Pagination: pagination, + }, nil + +}