diff --git a/gpp_parsed_consent.go b/gpp_parsed_consent.go index b077503..7e810b9 100644 --- a/gpp_parsed_consent.go +++ b/gpp_parsed_consent.go @@ -3,16 +3,53 @@ package iabconsent import ( "encoding/base64" "fmt" + "strings" "github.com/pkg/errors" ) +// GppHeader is the first section of a GPP Consent String. +// See ParseGppHeader for in-depth format. type GppHeader struct { Type int Version int Sections []int } +// GppParsedConsent is an empty interface since GPP will need to handle more consent structs +// than just the Multi-state Privacy Agreement structs. +type GppParsedConsent interface { +} + +// GppSection contains the specific Section ID (important to match up correct parsing). +// and pre-parsed Section Value, including all subsections. +type GppSection struct { + sectionId int + sectionValue string +} + +type GppSectionParser interface { + ParseConsent() (GppParsedConsent, error) + GetSectionId() int +} + +// GetSectionId returns the Section ID for a given GppSection. +func (g *GppSection) GetSectionId() int { + return g.sectionId +} + +type GppSubSection struct { + // Global Privacy Control (GPC) is signaled and set. + Gpc bool +} + +type GppSubSectionTypes int + +const ( + SubSectCore GppSubSectionTypes = iota + SubSectGpc +) + // ParseGppHeader parses the first (and required) part of any GPP Consent String. // It is used to read the Type, Version, and which sections are contained in the following string(s). // Format is: @@ -42,3 +79,114 @@ func ParseGppHeader(s string) (*GppHeader, error) { g.Sections, _ = r.ReadFibonacciRange() return g, r.Err } + +// MapGppSectionToParser takes a base64 Raw URL Encoded string which represents a GPP v1 string +// of the format {gpp header}~{section 1}[.{sub-section}][~{section n}] +// and returns each pair of section value and parsing function that should be used. +// The pairs are returned to allow more control over how parsing functions are applied. +func MapGppSectionToParser(s string) ([]GppSectionParser, error) { + var gppHeader *GppHeader + var err error + // ~ separated fields. with the format {gpp header}~{section 1}[.{sub-section}][~{section n}] + var segments = strings.Split(s, "~") + if len(segments) < 2 { + return nil, errors.New("not enough gpp segments") + } + + gppHeader, err = ParseGppHeader(segments[0]) + if err != nil { + return nil, errors.Wrap(err, "read gpp header") + } else if len(segments[1:]) != len(gppHeader.Sections) { + // Return early if sections in header do not match sections passed. + return nil, errors.New("mismatch number of sections") + } + // Go through each section and add parsing function and section value to returned value. + var gppSections = make([]GppSectionParser, 0) + for i := 1; i < len(segments); i++ { + var gppSection GppSectionParser + switch sid := gppHeader.Sections[i-1]; sid { + case 7: + gppSection = NewMspaNationl(segments[i]) + default: + // Skip if no matching struct, as Section ID is not supported yet. + // Any newly supported Section IDs should be added as cases here. + } + if gppSection != nil { + gppSections = append(gppSections, gppSection) + } + } + return gppSections, nil +} + +// ParseGppConsent takes a base64 Raw URL Encoded string which represents a GPP v1 string and +// returns a map of Section ID to ParsedConsents with consent parsed via a consecutive parsing. +func ParseGppConsent(s string) (map[int]GppParsedConsent, error) { + var gppSections []GppSectionParser + var err error + gppSections, err = MapGppSectionToParser(s) + if err != nil { + return nil, err + } + var gppConsents = make(map[int]GppParsedConsent, len(gppSections)) + // Consecutively, go through each section and try to parse. + for _, gpp := range gppSections { + var consent GppParsedConsent + var consentErr error + consent, consentErr = gpp.ParseConsent() + if consentErr != nil { + // If an error, quietly do not add teh consent value to map. + } else { + gppConsents[gpp.GetSectionId()] = consent + } + } + return gppConsents, nil +} + +// ParseGppSubSections parses the subsections that may be appended to GPP sections after a `.` +// Currently, GPC is the only subsection, so we only have a single Subsection parsing function. +// In the future, Section IDs may need their own SubSection parser. +func ParseGppSubSections(subSections []string) (*GppSubSection, error) { + var gppSub = new(GppSubSection) + // There could be >1 subsection, but we will only return a single GppSubSection result. + for _, s := range subSections { + // Actual base64 encoded data, so no need to add extra `0`s. + var b, err = base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, errors.Wrap(err, "parse gpp subsection string") + } + var r = NewConsentReader(b) + + var subType int + subType, err = r.ReadInt(2) + if err != nil { + return nil, errors.Wrap(err, "parse gpp subsection type") + } + // Check for specific SubSection Type, and then parse subsection correctly. + switch GppSubSectionTypes(subType) { + case SubSectGpc: + var gppValue bool + gppValue, err = ParseGpcSubsection(r) + if err != nil { + return nil, errors.Wrap(err, "parse gpp subsection gpc bool") + } + // Only override if not set to true already, as we want the most restrictive value + // if > 1 GPC subsection. + if gppSub.Gpc != true { + gppSub.Gpc = gppValue + } + } + } + return gppSub, nil +} + +// ParseGpcSubsection reads the next bit as a bool, and returns the result of value. +// Info about GPC subsection here: https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/606b99efc16b649c5c1f8f1d2eb0d0d3258c4a2d/Sections/US-National/IAB%20Privacy%E2%80%99s%20National%20Privacy%20Technical%20Specification.md#gpc-sub-section +func ParseGpcSubsection(r *ConsentReader) (bool, error) { + var gppValue bool + var err error + gppValue, err = r.ReadBool() + if err != nil { + return false, errors.Wrap(err, "parse gpp subsection gpc bool") + } + return gppValue, err +} diff --git a/gpp_parsed_consent_fixture_test.go b/gpp_parsed_consent_fixture_test.go new file mode 100644 index 0000000..f875781 --- /dev/null +++ b/gpp_parsed_consent_fixture_test.go @@ -0,0 +1,160 @@ +package iabconsent_test + +import ( + "github.com/LiveRamp/iabconsent" +) + +// Test fixtures can be created here: https://iabgpp.com/ + +var gppParsedConsentFixtures = map[string]map[int]*iabconsent.MspaParsedConsent{ + // Valid GPP w/ US National MSPA, No Subsection. + "DBABL~BVVqAAEABAA": {7: { + Version: 1, + SharingNotice: iabconsent.NoticeProvided, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + TargetedAdvertisingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataProcessingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: iabconsent.NotOptedOut, + TargetedAdvertisingOptOut: iabconsent.NotOptedOut, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.ConsentNotApplicable, + 4: iabconsent.ConsentNotApplicable, + 5: iabconsent.ConsentNotApplicable, + 6: iabconsent.ConsentNotApplicable, + 7: iabconsent.NoConsent, + 8: iabconsent.ConsentNotApplicable, + 9: iabconsent.ConsentNotApplicable, + 10: iabconsent.ConsentNotApplicable, + 11: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.NoConsent, + MspaCoveredTransaction: iabconsent.MspaNotApplicable, + MspaOptOutOptionMode: iabconsent.MspaNotApplicable, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + Gpc: false, + }, + }, + // Valid GPP w/ US National MSPA, Subsection of GPC False. + "DBABL~BVVqAAEABAA.QA": {7: { + Version: 1, + SharingNotice: iabconsent.NoticeProvided, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + TargetedAdvertisingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataProcessingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: iabconsent.NotOptedOut, + TargetedAdvertisingOptOut: iabconsent.NotOptedOut, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.ConsentNotApplicable, + 4: iabconsent.ConsentNotApplicable, + 5: iabconsent.ConsentNotApplicable, + 6: iabconsent.ConsentNotApplicable, + 7: iabconsent.NoConsent, + 8: iabconsent.ConsentNotApplicable, + 9: iabconsent.ConsentNotApplicable, + 10: iabconsent.ConsentNotApplicable, + 11: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.NoConsent, + MspaCoveredTransaction: iabconsent.MspaNotApplicable, + MspaOptOutOptionMode: iabconsent.MspaNotApplicable, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + Gpc: false, + }, + }, + // Valid GPP w/ US National MSPA, Subsection of GPC True. + "DBABL~BVVqAAEABAA.YA": {7: { + Version: 1, + SharingNotice: iabconsent.NoticeProvided, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + TargetedAdvertisingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataProcessingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: iabconsent.NotOptedOut, + TargetedAdvertisingOptOut: iabconsent.NotOptedOut, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.ConsentNotApplicable, + 4: iabconsent.ConsentNotApplicable, + 5: iabconsent.ConsentNotApplicable, + 6: iabconsent.ConsentNotApplicable, + 7: iabconsent.NoConsent, + 8: iabconsent.ConsentNotApplicable, + 9: iabconsent.ConsentNotApplicable, + 10: iabconsent.ConsentNotApplicable, + 11: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.NoConsent, + MspaCoveredTransaction: iabconsent.MspaNotApplicable, + MspaOptOutOptionMode: iabconsent.MspaNotApplicable, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + Gpc: true, + }, + }, + // Valid GPP string w/ sections for EU TCF V2 and US Privacy + // Since both are not supported, Consent fixture should be blank. + "DBACNY~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN": {}, + // Valid GPP w/ US National MSPA and US Privacy, but skip US Privacy until supported. + "DBABzw~1YNN~BVVqAAEABAA.QA": {7: { + Version: 1, + SharingNotice: iabconsent.NoticeProvided, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + TargetedAdvertisingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataProcessingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: iabconsent.NotOptedOut, + TargetedAdvertisingOptOut: iabconsent.NotOptedOut, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.ConsentNotApplicable, + 4: iabconsent.ConsentNotApplicable, + 5: iabconsent.ConsentNotApplicable, + 6: iabconsent.ConsentNotApplicable, + 7: iabconsent.NoConsent, + 8: iabconsent.ConsentNotApplicable, + 9: iabconsent.ConsentNotApplicable, + 10: iabconsent.ConsentNotApplicable, + 11: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.NoConsent, + MspaCoveredTransaction: iabconsent.MspaNotApplicable, + MspaOptOutOptionMode: iabconsent.MspaNotApplicable, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + }, + }, +} diff --git a/gpp_parsed_consent_test.go b/gpp_parsed_consent_test.go index 1f9f8fd..835f904 100644 --- a/gpp_parsed_consent_test.go +++ b/gpp_parsed_consent_test.go @@ -1,6 +1,9 @@ package iabconsent_test import ( + "encoding/base64" + "strings" + "github.com/go-check/check" "github.com/pkg/errors" @@ -50,6 +53,14 @@ func (s *GppParseSuite) TestParseGppHeader(c *check.C) { Version: 1, Sections: []int{7}}, }, + { + description: "US Privacy and US National MSPA (Multi-State Privacy Agreement)", + header: "DBABzw", + expected: &iabconsent.GppHeader{ + Type: 3, + Version: 1, + Sections: []int{6, 7}}, + }, } for _, tc := range tcs { @@ -60,7 +71,7 @@ func (s *GppParseSuite) TestParseGppHeader(c *check.C) { } } -func (s *GppParseSuite) TestParseGppHeaderFail(c *check.C) { +func (s *GppParseSuite) TestParseGppHeaderError(c *check.C) { var tcs = []struct { description string header string @@ -99,3 +110,163 @@ func (s *GppParseSuite) TestParseGppHeaderFail(c *check.C) { c.Check(err.Error(), check.Equals, tc.expected.Error()) } } + +func (s *MspaSuite) TestMapGppSectionToParser(c *check.C) { + for gppString, expectedValues := range gppParsedConsentFixtures { + c.Log(gppString) + + var gppSections, err = iabconsent.MapGppSectionToParser(gppString) + + c.Check(err, check.IsNil) + // Instead of checking the parsing functions, run each of them to ensure the final values match. + c.Check(gppSections, check.HasLen, len(expectedValues)) + for _, sect := range gppSections { + consent, err := sect.ParseConsent() + c.Check(err, check.IsNil) + c.Check(consent, check.DeepEquals, expectedValues[sect.GetSectionId()]) + } + } +} + +func (s *MspaSuite) TestParseGppConsent(c *check.C) { + for g, e := range gppParsedConsentFixtures { + c.Log(g) + + var p, err = iabconsent.ParseGppConsent(g) + + c.Check(err, check.IsNil) + c.Check(p, check.HasLen, len(e)) + for i, expected := range e { + parsed, found := p[i] + c.Check(found, check.Equals, true) + c.Check(parsed, check.DeepEquals, expected) + } + } +} + +func (s *MspaSuite) TestParseGppErrors(c *check.C) { + tcs := []struct { + desc string + gpp string + expected error + }{ + { + desc: "No sections.", + gpp: "DBABL", + expected: errors.New("not enough gpp segments"), + }, + { + desc: "Mismatched # of sections, header expects 1.", + gpp: "DBABL~section1~section2", + expected: errors.New("mismatch number of sections"), + }, + { + desc: "Bad header.", + gpp: "badheader~BVVqAAEABAA.QA", + expected: errors.New("read gpp header: wrong gpp header type 27"), + }, + } + for _, t := range tcs { + c.Log(t.desc) + + var p, err = iabconsent.MapGppSectionToParser(t.gpp) + + c.Check(p, check.IsNil) + c.Check(err, check.ErrorMatches, t.expected.Error()) + } +} + +func (s *GppParseSuite) TestParseGppSubSections(c *check.C) { + var tcs = []struct { + description string + subsections string + expected *iabconsent.GppSubSection + }{ + { + description: "GPC Type, false value", + // 01000000 + subsections: "QA", + expected: &iabconsent.GppSubSection{ + Gpc: false, + }, + }, + { + description: "GPC Type, true value.", + // 01100000 + subsections: "YA", + expected: &iabconsent.GppSubSection{ + Gpc: true, + }, + }, + { + description: "No GPC Type.", + // 00000000 + subsections: "AA", + expected: &iabconsent.GppSubSection{ + Gpc: false, + }, + }, + { + description: "GPC True, then GPC False, should remain True.", + // 01100000.01000000 + subsections: "YA.QA", + expected: &iabconsent.GppSubSection{ + Gpc: true, + }, + }, + { + description: "GPC False, then GPC True, should remain True.", + // 01000000.01100000 + subsections: "QA.YA", + expected: &iabconsent.GppSubSection{ + Gpc: true, + }, + }, + } + + for _, tc := range tcs { + c.Log(tc) + // There may be >1 subsections, and func expects them as an array, so split. + subsect := strings.Split(tc.subsections, ".") + var g, err = iabconsent.ParseGppSubSections(subsect) + c.Check(err, check.IsNil) + c.Check(g, check.DeepEquals, tc.expected) + } +} + +func (s *GppParseSuite) TestParseGpcSubSections(c *check.C) { + var tcs = []struct { + description string + subsection string + expected bool + }{ + { + description: "All 0 bits.", + // 0000000 + subsection: "AA", + expected: false, + }, + { + description: "Second bit 1.", + // 01000000 + subsection: "QA", + expected: false, + }, + { + description: "First bit 1.", + // 1000000 + subsection: "gA", + expected: true, + }, + } + + for _, tc := range tcs { + c.Log(tc) + b, err := base64.RawURLEncoding.DecodeString(tc.subsection) + c.Check(err, check.IsNil) + var cr = iabconsent.NewConsentReader(b) + g, err := iabconsent.ParseGpcSubsection(cr) + c.Check(err, check.IsNil) + c.Check(g, check.DeepEquals, tc.expected) + } +} diff --git a/mspa_parsed_consent.go b/mspa_parsed_consent.go new file mode 100644 index 0000000..0215f14 --- /dev/null +++ b/mspa_parsed_consent.go @@ -0,0 +1,181 @@ +package iabconsent + +// MspaParsedConsent represents data extract from a Multi-State Privacy Agreement (mspa) consent string. +// Format can be found here: https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Sections/US-National/IAB%20Privacy%E2%80%99s%20National%20Privacy%20Technical%20Specification.md#core-segment +type MspaParsedConsent struct { + // The version of this section specification used to encode the string. + Version int + // Notice of the Sharing of the Consumer’s Personal Data with Third Parties. + // 0 Not Applicable. The Business does not share Personal Data with Third Parties. + // 1 Yes, notice was provided + // 2 No, notice was not provided + SharingNotice MspaNotice + // Notice of the Opportunity to Opt Out of the Sale of the Consumer’s Personal Data. + // 0 Not Applicable. The Business does not Sell Personal Data. + // 1 Yes, notice was provided + // 2 No, notice was not provided + SaleOptOutNotice MspaNotice + // Notice of the Opportunity to Opt Out of the Sharing of the Consumer’s Personal Data. + // 0 Not Applicable.The Business does not Share Personal Data. + // 1 Yes, notice was provided + // 2 No, notice was not provided + SharingOptOutNotice MspaNotice + // Notice of the Opportunity to Opt Out of Processing of the Consumer’s Personal Data for Targeted Advertising. + // 0 Not Applicable.The Business does not Process Personal Data for Targeted Advertising. + // 1 Yes, notice was provided + // 2 No, notice was not provided + TargetedAdvertisingOptOutNotice MspaNotice + // Notice of the Opportunity to Opt Out of the Processing of the Consumer’s Sensitive Data. + // 0 Not Applicable. The Business does not Process Sensitive Data. + // 1 Yes, notice was provided + // 2 No, notice was not provided + SensitiveDataProcessingOptOutNotice MspaNotice + // Notice of the Opportunity to Limit Use or Disclosure of the Consumer’s Sensitive Data. + // 0 Not Applicable. The Business does not use or disclose Sensitive Data. + // 1 Yes, notice was provided + // 2 No, notice was not provided + SensitiveDataLimitUseNotice MspaNotice + // Opt-Out of the Sale of the Consumer’s Personal Data. + // 0 Not Applicable. SaleOptOutNotice value was not applicable or no notice was provided + // 1 Opted Out + // 2 Did Not Opt Out + SaleOptOut MspaOptout + // Opt-Out of the Sharing of the Consumer’s Personal Data. + // 0 Not Applicable. SharingOptOutNotice value was not applicable or no notice was provided. + // 1 Opted Out + // 2 Did Not Opt Out + SharingOptOut MspaOptout + // Opt-Out of Processing the Consumer’s Personal Data for Targeted Advertising. + // 0 Not Applicable. TargetedAdvertisingOptOutNotice value was not applicable or no notice was provided + // 1 Opted Out + // 2 Did Not Opt Out + TargetedAdvertisingOptOut MspaOptout + // Two bits for each Data Activity: + // 0 Not Applicable. The Business does not Process the specific category of Sensitive Data. + // 1 No Consent + // 2 Consent + // Data Activities: + // (1) Consent to Process the Consumer’s Sensitive Data Consisting of Personal Data Revealing Racial or Ethnic Origin. + // (2) Consent to Process the Consumer’s Sensitive Data Consisting of Personal Data Revealing Religious or Philosophical Beliefs. + // (3) Consent to Process the Consumer’s Sensitive Data Consisting of Personal Data Concerning a Consumer’s Health (including a Mental or Physical Health Condition or Diagnosis; Medical History; or Medical Treatment or Diagnosis by a Health Care Professional). + // (4) Consent to Process the Consumer’s Sensitive Data Consisting of Personal Data Revealing Sex Life or Sexual Orientation. + // (5) Consent to Process the Consumer’s Sensitive Data Consisting of Personal Data Revealing Citizenship or Immigration Status. + // (6) Consent to Process the Consumer’s Sensitive Data Consisting of Genetic Data for the Purpose of Uniquely Identifying an Individual / Natural Person. + // (7) Consent to Process the Consumer’s Sensitive Data Consisting of Biometric Data for the Purpose of Uniquely Identifying an Individual / Natural Person. + // (8) Consent to Process the Consumer’s Sensitive Data Consisting of Precise Geolocation Data. + // (9) Consent to Process the Consumer’s Sensitive Data Consisting of a Consumer’s Social Security, Driver’s License, State Identification Card, or Passport Number. + // (10) Consent to Process the Consumer’s Sensitive Data Consisting of a Consumer’s Account Log-In, Financial Account, Debit Card, or Credit Card Number in Combination with Any Required Security or Access Code, Password, or Credentials Allowing Access to an Account. + // (11) Consent to Process the Consumer’s Sensitive Data Consisting of Union Membership. + // (12) Consent to Process the Consumer’s Sensitive Data Consisting of the contents of a Consumer’s Mail, Email, and Text Messages unless You Are the Intended Recipient of the Communication. + SensitiveDataProcessing map[int]MspaConsent + // Two bits for each Data Activity: + // 0 Not Applicable. The Business does not have actual knowledge that it Processes Personal Data or Sensitive Data of a Consumer who is a known child. + // 1 No Consent + // 2 Consent + // Fields: + // (1) Consent to Process the Consumer’s Personal Data or Sensitive Data for Consumers from Age 13 to 16. + // (2) Consent to Process the Consumer’s Personal Data or Sensitive Data for Consumers Younger Than 13 Years of Age. + KnownChildSensitiveDataConsents map[int]MspaConsent + // Consent to Collection, Use, Retention, Sale, and/or Sharing of the Consumer’s Personal Data that Is Unrelated to or Incompatible with the Purpose(s) for which the Consumer’s Personal Data Was Collected or Processed. + // 0 Not Applicable. The Business does not use, retain, Sell, or Share the Consumer’s Personal Data for advertising purposes that are unrelated to or incompatible with the purpose(s) for which the Consumer’s Personal Data was collected or processed. + // 1 No Consent + // 2 Consent + PersonalDataConsents MspaConsent + // Publisher or Advertiser, as applicable, is a signatory to the IAB Multistate Service Provider Agreement (MSPA), as may be amended from time to time, and declares that the transaction is a “Covered Transaction” as defined in the MSPA. + // 1 Yes + // 2 No + MspaCoveredTransaction MspaNaYesNo + // Publisher or Advertiser, as applicable, has enabled “Opt-Out Option Mode” for the “Covered Transaction,” as such terms are defined in the MSPA. + // 0 Not Applicable. + // 1 Yes + // 2 No + MspaOptOutOptionMode MspaNaYesNo + // Publisher or Advertiser, as applicable, has enabled “Service Provider Mode” for the “Covered Transaction,” as such terms are defined in the MSPA. + // 0 Not Applicable + // 1 Yes + // 2 No + MspaServiceProviderMode MspaNaYesNo + // Subsections added below: + // Global Privacy Control (GPC) is signaled and set. + Gpc bool +} + +type MspaNotice int + +const ( + NoticeNotApplicable MspaNotice = iota + NoticeProvided + NoticeNotProvided + InvalidNoticeValue +) + +type MspaOptout int + +const ( + OptOutNotApplicable MspaOptout = iota + OptedOut + NotOptedOut + InvalidOptOutValue +) + +type MspaConsent int + +const ( + ConsentNotApplicable MspaConsent = iota + NoConsent + Consent + InvalidConsentValue +) + +// MspaNaYesNo represents common values for MSPA values representing +// answers, Not Applicable, Yes, No (in that order). +type MspaNaYesNo int + +const ( + MspaNotApplicable MspaNaYesNo = iota + MspaYes + MspaNo + InvalidMspaValue +) + +// ReadMspaNotice reads integers into standard MSPA Notice values of +// 0: Not applicable, 1: Yes, notice was provided, 2: No, notice was not provided. +func (r *ConsentReader) ReadMspaNotice() (MspaNotice, error) { + var mn, err = r.ReadInt(2) + return MspaNotice(mn), err +} + +// ReadMspaOptOut reads integers into standard MSPA OptOut values of +// 0: Not Applicable, 1: Opted out, 2: Did not opt out +func (r *ConsentReader) ReadMspaOptOut() (MspaOptout, error) { + var mo, err = r.ReadInt(2) + return MspaOptout(mo), err +} + +// ReadMspaConsent reads integers into standard Consent values of +// 0: Not Applicable, 1: Not Consent, 2: Consent +func (r *ConsentReader) ReadMspaConsent() (MspaConsent, error) { + var mc, err = r.ReadInt(2) + return MspaConsent(mc), err +} + +// ReadMspaBitfieldConsent reads n-bitfield values, and converts the values into +// MSPA Consent values. +func (r *ConsentReader) ReadMspaBitfieldConsent(l uint) (map[int]MspaConsent, error) { + var bc, err = r.ReadNBitField(2, l) + var consentBitfield = make(map[int]MspaConsent, len(bc)) + if err != nil { + return nil, err + } + for i, b := range bc { + consentBitfield[i] = MspaConsent(b) + } + return consentBitfield, err +} + +// ReadMspaNaYesNo is a helper function to handle the responses to standard MSPA +// values that are in the same format of 0: Not Applicable, 1: Yes, 2: No. +func (r *ConsentReader) ReadMspaNaYesNo() (MspaNaYesNo, error) { + var nyn, err = r.ReadInt(2) + return MspaNaYesNo(nyn), err +} diff --git a/mspa_parsed_consent_fixture_test.go b/mspa_parsed_consent_fixture_test.go new file mode 100644 index 0000000..6ca348e --- /dev/null +++ b/mspa_parsed_consent_fixture_test.go @@ -0,0 +1,117 @@ +package iabconsent_test + +import ( + "github.com/LiveRamp/iabconsent" +) + +// Test fixtures can be created here: https://iabgpp.com/ + +var usNationalConsentFixtures = map[string]*iabconsent.MspaParsedConsent{ + // With subsection of GPC False. + "BVVqAAEABAA.QA": { + Version: 1, + SharingNotice: iabconsent.NoticeProvided, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + TargetedAdvertisingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataProcessingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: iabconsent.NotOptedOut, + TargetedAdvertisingOptOut: iabconsent.NotOptedOut, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.ConsentNotApplicable, + 4: iabconsent.ConsentNotApplicable, + 5: iabconsent.ConsentNotApplicable, + 6: iabconsent.ConsentNotApplicable, + 7: iabconsent.NoConsent, + 8: iabconsent.ConsentNotApplicable, + 9: iabconsent.ConsentNotApplicable, + 10: iabconsent.ConsentNotApplicable, + 11: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.NoConsent, + MspaCoveredTransaction: iabconsent.MspaNotApplicable, + MspaOptOutOptionMode: iabconsent.MspaNotApplicable, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + }, + // With subsection of GPC True. + "BVVqAAEABAA.YA": { + Version: 1, + SharingNotice: iabconsent.NoticeProvided, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + TargetedAdvertisingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataProcessingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: iabconsent.NotOptedOut, + TargetedAdvertisingOptOut: iabconsent.NotOptedOut, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.ConsentNotApplicable, + 4: iabconsent.ConsentNotApplicable, + 5: iabconsent.ConsentNotApplicable, + 6: iabconsent.ConsentNotApplicable, + 7: iabconsent.NoConsent, + 8: iabconsent.ConsentNotApplicable, + 9: iabconsent.ConsentNotApplicable, + 10: iabconsent.ConsentNotApplicable, + 11: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.NoConsent, + MspaCoveredTransaction: iabconsent.MspaNotApplicable, + MspaOptOutOptionMode: iabconsent.MspaNotApplicable, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + Gpc: true, + }, + // Without subsection. + "BqqAqqqqqqA": { + Version: 1, + SharingNotice: iabconsent.NoticeNotProvided, + SaleOptOutNotice: iabconsent.NoticeNotProvided, + SharingOptOutNotice: iabconsent.NoticeNotProvided, + TargetedAdvertisingOptOutNotice: iabconsent.NoticeNotProvided, + SensitiveDataProcessingOptOutNotice: iabconsent.NoticeNotProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeNotProvided, + SaleOptOut: iabconsent.OptOutNotApplicable, + SharingOptOut: iabconsent.OptOutNotApplicable, + TargetedAdvertisingOptOut: iabconsent.OptOutNotApplicable, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.Consent, + 1: iabconsent.Consent, + 2: iabconsent.Consent, + 3: iabconsent.Consent, + 4: iabconsent.Consent, + 5: iabconsent.Consent, + 6: iabconsent.Consent, + 7: iabconsent.Consent, + 8: iabconsent.Consent, + 9: iabconsent.Consent, + 10: iabconsent.Consent, + 11: iabconsent.Consent, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.Consent, + 1: iabconsent.Consent, + }, + PersonalDataConsents: iabconsent.Consent, + MspaCoveredTransaction: iabconsent.MspaNo, + MspaOptOutOptionMode: iabconsent.MspaNo, + MspaServiceProviderMode: iabconsent.MspaNo, + Gpc: false, + }, +} diff --git a/mspa_parsed_consent_test.go b/mspa_parsed_consent_test.go new file mode 100644 index 0000000..94178eb --- /dev/null +++ b/mspa_parsed_consent_test.go @@ -0,0 +1,145 @@ +package iabconsent_test + +import ( + "github.com/go-check/check" + "github.com/pkg/errors" + + "github.com/LiveRamp/iabconsent" +) + +type MspaSuite struct{} + +var _ = check.Suite(&MspaSuite{}) + +func (s *MspaSuite) TestReadMspaNotice(c *check.C) { + var r = iabconsent.NewConsentReader([]byte{0b00011011}) + var mns = []iabconsent.MspaNotice{ + iabconsent.NoticeNotApplicable, + iabconsent.NoticeProvided, + iabconsent.NoticeNotProvided, + iabconsent.InvalidNoticeValue, + } + for _, i := range mns { + var mn, err = r.ReadMspaNotice() + c.Check(err, check.IsNil) + c.Check(mn, check.Equals, i) + } +} + +func (s *MspaSuite) TestReadMspaOptOut(c *check.C) { + var r = iabconsent.NewConsentReader([]byte{0b00011011}) + var mos = []iabconsent.MspaOptout{ + iabconsent.OptOutNotApplicable, + iabconsent.OptedOut, + iabconsent.NotOptedOut, + iabconsent.InvalidOptOutValue, + } + for _, i := range mos { + var mo, err = r.ReadMspaOptOut() + c.Check(err, check.IsNil) + c.Check(mo, check.Equals, i) + } +} + +func (s *MspaSuite) TestReadMspaConsent(c *check.C) { + var r = iabconsent.NewConsentReader([]byte{0b00011011}) + var mcs = []iabconsent.MspaConsent{ + iabconsent.ConsentNotApplicable, + iabconsent.NoConsent, + iabconsent.Consent, + iabconsent.InvalidConsentValue, + } + for _, i := range mcs { + var mc, err = r.ReadMspaConsent() + c.Check(err, check.IsNil) + c.Check(mc, check.Equals, i) + } +} + +func (s *MspaSuite) TestReadMspaBitfieldConsent(c *check.C) { + var tcs = []struct { + testBytes []byte + bitfieldLength uint + expected map[int]iabconsent.MspaConsent + }{ + {testBytes: []byte{0b00000000}, + bitfieldLength: 1, + expected: map[int]iabconsent.MspaConsent{0: iabconsent.ConsentNotApplicable}}, + {testBytes: []byte{0b00000000}, + bitfieldLength: 4, + expected: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.ConsentNotApplicable}}, + {testBytes: []byte{0b00011011}, + bitfieldLength: 4, + expected: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.NoConsent, + 2: iabconsent.Consent, + 3: iabconsent.InvalidConsentValue}}, + } + + for _, t := range tcs { + var r = iabconsent.NewConsentReader(t.testBytes) + var bc, err = r.ReadMspaBitfieldConsent(t.bitfieldLength) + c.Check(err, check.IsNil) + c.Check(bc, check.DeepEquals, t.expected) + } +} + +func (s *MspaSuite) TestReadMspaNaYesNo(c *check.C) { + var r = iabconsent.NewConsentReader([]byte{0b00011011}) + var mcs = []iabconsent.MspaNaYesNo{ + iabconsent.MspaNotApplicable, + iabconsent.MspaYes, + iabconsent.MspaNo, + iabconsent.InvalidMspaValue, + } + for _, i := range mcs { + var mc, err = r.ReadMspaNaYesNo() + c.Check(err, check.IsNil) + c.Check(mc, check.Equals, i) + } +} + +func (s *MspaSuite) TestParseUsNational(c *check.C) { + for k, v := range usNationalConsentFixtures { + c.Log(k) + + var gppSection = iabconsent.NewMspaNationl(k) + var p, err = gppSection.ParseConsent() + + c.Check(err, check.IsNil) + c.Check(p, check.DeepEquals, v) + } +} + +func (s *MspaSuite) TestParseUsNationalError(c *check.C) { + var tcs = []struct { + desc string + usNatString string + expected error + }{ + { + desc: "Wrong Version.", + usNatString: "DVVqAAEABA", + expected: errors.New("non-v1 string passed."), + }, + { + desc: "Bad Decoding.", + usNatString: "$%&*(", + expected: errors.New("parse usnat consent string: illegal base64 data at input byte 0"), + }, + } + for _, t := range tcs { + c.Log(t.desc) + + var gppSection = iabconsent.NewMspaNationl(t.usNatString) + var p, err = gppSection.ParseConsent() + + c.Check(p, check.IsNil) + c.Check(err, check.ErrorMatches, t.expected.Error()) + } +} diff --git a/mspa_sections.go b/mspa_sections.go new file mode 100644 index 0000000..38ab234 --- /dev/null +++ b/mspa_sections.go @@ -0,0 +1,63 @@ +package iabconsent + +import ( + "encoding/base64" + "strings" + + "github.com/pkg/errors" +) + +type MspaUsNational struct { + GppSection +} + +func NewMspaNationl(section string) *MspaUsNational { + return &MspaUsNational{GppSection{sectionId: 7, sectionValue: section}} +} + +func (m *MspaUsNational) ParseConsent() (GppParsedConsent, error) { + var segments = strings.Split(m.sectionValue, ".") + + var b, err = base64.RawURLEncoding.DecodeString(segments[0]) + if err != nil { + return nil, errors.Wrap(err, "parse usnat consent string") + } + + var r = NewConsentReader(b) + + // This block of code directly describes the format of the payload. + // The spec for the consent string can be found here: + // https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/tree/main/Sections/US-National#core-segment + var p = &MspaParsedConsent{} + p.Version, _ = r.ReadInt(6) + + if p.Version != 1 { + return nil, errors.New("non-v1 string passed.") + } + + p.SharingNotice, _ = r.ReadMspaNotice() + p.SaleOptOutNotice, _ = r.ReadMspaNotice() + p.SharingOptOutNotice, _ = r.ReadMspaNotice() + p.TargetedAdvertisingOptOutNotice, _ = r.ReadMspaNotice() + p.SensitiveDataProcessingOptOutNotice, _ = r.ReadMspaNotice() + p.SensitiveDataLimitUseNotice, _ = r.ReadMspaNotice() + p.SaleOptOut, _ = r.ReadMspaOptOut() + p.SharingOptOut, _ = r.ReadMspaOptOut() + p.TargetedAdvertisingOptOut, _ = r.ReadMspaOptOut() + p.SensitiveDataProcessing, _ = r.ReadMspaBitfieldConsent(12) + p.KnownChildSensitiveDataConsents, _ = r.ReadMspaBitfieldConsent(2) + p.PersonalDataConsents, _ = r.ReadMspaConsent() + p.MspaCoveredTransaction, _ = r.ReadMspaNaYesNo() + // 0 is not a valid value according to the docs for MspaCoveredTransaction. Instead of erroring, + // return the value of the string, and let downstream processing handle if the value is 0. + p.MspaOptOutOptionMode, _ = r.ReadMspaNaYesNo() + p.MspaServiceProviderMode, _ = r.ReadMspaNaYesNo() + + if len(segments) > 1 { + var gppSubsectionConsent *GppSubSection + gppSubsectionConsent, _ = ParseGppSubSections(segments[1:]) + p.Gpc = gppSubsectionConsent.Gpc + } + + return p, r.Err +}