diff --git a/ccpa_parsed_consent.go b/ccpa_parsed_consent.go new file mode 100644 index 0000000..0227609 --- /dev/null +++ b/ccpa_parsed_consent.go @@ -0,0 +1,44 @@ +package iabconsent + +import "github.com/pkg/errors" + +const ( + CCPAYes = 'Y' + CCPANo = 'N' + CCPANotApplicable = '-' + CCPAVersion = 1 +) + +// CcpaParsedConsent represents data extract from a California Consumer Privacy Act (CCPA) consent string. +// Format can be found here: https://github.com/InteractiveAdvertisingBureau/USPrivacy/blob/master/CCPA/US%20Privacy%20String.md +type CcpaParsedConsent struct { + // The version of this string specification used to encode the string + Version int + + // N = No, Y = Yes, - = Not Applicable + Notice uint8 + + // N = No, Y = Yes, - = Not Applicable; For use ONLY when CCPA does not apply. + OptOutSale uint8 + + // 0 = No, 1 = Yes, - = Not Applicable + LSPACoveredTransaction uint8 +} + +func IsValidCCPAString(ccpaString string) (bool, error) { + if len(ccpaString) != 4 { + return false, errors.Wrap(nil, "invalid uspv consent string length") + } + + if ccpaString[0]-'0' != CCPAVersion { + return false, errors.Wrap(nil, "invalid uspv consent string version") + } + + for i := 1; i < 4; i++ { + if ccpaString[i] != CCPAYes && ccpaString[i] != CCPANo && ccpaString[i] != CCPANotApplicable { + return false, errors.Wrap(nil, "invalid uspv consent string") + } + } + + return true, nil +} diff --git a/gpp_parsed_consent.go b/gpp_parsed_consent.go index 0d741a9..ee9e70c 100644 --- a/gpp_parsed_consent.go +++ b/gpp_parsed_consent.go @@ -8,6 +8,18 @@ import ( "github.com/pkg/errors" ) +const ( + SectionIDEUTCFv2 = 2 + SectionIDCANTCF = 5 + SectionIDUSPV1 = 6 + SectionIDUSNAT = 7 + SectionIDUSCA = 8 + SectionIDUSVA = 9 + SectionIDUSCO = 10 + SectionIDUSUT = 11 + SectionIDUSCT = 12 +) + // GppHeader is the first section of a GPP Consent String. // See ParseGppHeader for in-depth format. type GppHeader struct { @@ -105,13 +117,28 @@ func MapGppSectionToParser(s string) ([]GppSectionParser, error) { for i := 1; i < len(segments); i++ { var gppSection GppSectionParser switch sid := gppHeader.Sections[i-1]; sid { - case 7: + case SectionIDEUTCFv2: + gppSection = NewTCFEU(segments[i]) + case SectionIDCANTCF: + gppSection = NewTCFCA(segments[i]) + case SectionIDUSPV1: + gppSection = NewUSPV(segments[i]) + case SectionIDUSNAT: gppSection = NewMspaNational(segments[i]) - case 9: + case SectionIDUSCA: + gppSection = NewMspaCA(segments[i]) + case SectionIDUSVA: gppSection = NewMspaVA(segments[i]) + case SectionIDUSCO: + gppSection = NewMspaCO(segments[i]) + case SectionIDUSUT: + gppSection = NewMspaUT(segments[i]) + case SectionIDUSCT: + gppSection = NewMspaCT(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. + // TODO: GPP error handling } if gppSection != nil { gppSections = append(gppSections, gppSection) @@ -136,6 +163,7 @@ func ParseGppConsent(s string) (map[int]GppParsedConsent, error) { var consentErr error consent, consentErr = gpp.ParseConsent() if consentErr != nil { + // TODO: GPP error handling // If an error, quietly do not add the consent value to map. } else { gppConsents[gpp.GetSectionId()] = consent diff --git a/gpp_parsed_consent_fixture_test.go b/gpp_parsed_consent_fixture_test.go index 1e4caf6..8d030b9 100644 --- a/gpp_parsed_consent_fixture_test.go +++ b/gpp_parsed_consent_fixture_test.go @@ -2,6 +2,7 @@ package iabconsent_test import ( "github.com/LiveRamp/iabconsent" + "time" ) // Test fixtures can be created here: https://iabgpp.com/ @@ -146,42 +147,43 @@ var gppParsedConsentFixtures = map[string]map[int]*iabconsent.MspaParsedConsent{ }, }, // Valid GPP w/ US US National and Virgina MSPA, Subsection of GPC False. - "DBACLMA~BVVqAAEABAA~BVoYYYA": {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, + "DBACLMA~BVVqAAEABAA~BVoYYYA": { + iabconsent.SectionIDUSNAT: { + 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, }, - PersonalDataConsents: iabconsent.NoConsent, - MspaCoveredTransaction: iabconsent.MspaNotApplicable, - MspaOptOutOptionMode: iabconsent.MspaNotApplicable, - MspaServiceProviderMode: iabconsent.MspaNotApplicable, - Gpc: false, - }, - 9: { + iabconsent.SectionIDUSVA: { Version: 1, SharingNotice: iabconsent.NoticeProvided, SaleOptOutNotice: iabconsent.NoticeProvided, @@ -207,43 +209,214 @@ var gppParsedConsentFixtures = map[string]map[int]*iabconsent.MspaParsedConsent{ Gpc: false, }, }, + // + "DBABBg~BVoAAACQ.QA": { + iabconsent.SectionIDUSCA: { + Version: 1, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: 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.ConsentNotApplicable, + 8: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.ConsentNotApplicable, + MspaCoveredTransaction: iabconsent.MspaNo, + MspaOptOutOptionMode: iabconsent.MspaYes, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + }, + }, +} + +// mixed of TCF, MSPA, US Privacy and CCPA +var gppParsedMixedConsent = map[string]map[int]iabconsent.GppParsedConsent{ // 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, + "DBACNYA~CPXuQIAPXuQIAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA.YAAAAAAAAAAA~1YNN": { + iabconsent.SectionIDEUTCFv2: &iabconsent.V2ParsedConsent{ + Version: 2, + Created: time.Date(2022, time.April, 20, 0, 0, 0, 0, time.UTC), + LastUpdated: time.Date(2022, time.April, 20, 0, 0, 0, 0, time.UTC), + CMPID: 31, + CMPVersion: 640, + ConsentScreen: 1, + ConsentLanguage: "EN", + VendorListVersion: 126, + TCFPolicyVersion: 2, + IsServiceSpecific: true, + UseNonStandardStacks: false, + SpecialFeaturesOptIn: map[int]bool{}, + PurposesConsent: map[int]bool{}, + PurposesLITransparency: map[int]bool{}, + PurposeOneTreatment: false, + PublisherCC: "DE", + MaxConsentVendorID: 0, + IsConsentRangeEncoding: false, + ConsentedVendors: map[int]bool{}, + NumConsentEntries: 0, + ConsentedVendorsRange: []*iabconsent.RangeEntry(nil), + MaxInterestsVendorID: 0, + IsInterestsRangeEncoding: false, + InterestsVendors: map[int]bool{}, + NumInterestsEntries: 0, + InterestsVendorsRange: []*iabconsent.RangeEntry(nil), + NumPubRestrictions: 0, + PubRestrictionEntries: []*iabconsent.PubRestrictionEntry{}, + OOBDisclosedVendors: (*iabconsent.OOBVendorList)(nil), + OOBAllowedVendors: (*iabconsent.OOBVendorList)(nil), + PublisherTCEntry: &iabconsent.PublisherTCEntry{ + SegmentType: 3, + PubPurposesConsent: map[int]bool{}, + PubPurposesLITransparency: map[int]bool{}, + NumCustomPurposes: 0, + CustomPurposesConsent: map[int]bool{}, + CustomPurposesLITransparency: map[int]bool{}, + }, }, - KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ - 0: iabconsent.ConsentNotApplicable, - 1: iabconsent.ConsentNotApplicable, + iabconsent.SectionIDUSPV1: &iabconsent.CcpaParsedConsent{ + Version: 1, + Notice: iabconsent.CCPAYes, + OptOutSale: iabconsent.CCPANo, + LSPACoveredTransaction: iabconsent.CCPANo, + }, + }, + //Valid GPP w/ US National MSPA and US Privacy + "DBABzw~1YNN~BVVqAAEABAA.QA": { + iabconsent.SectionIDUSPV1: &iabconsent.CcpaParsedConsent{ + Version: 1, + Notice: iabconsent.CCPAYes, + OptOutSale: iabconsent.CCPANo, + LSPACoveredTransaction: iabconsent.CCPANo, + }, + iabconsent.SectionIDUSNAT: &iabconsent.MspaParsedConsent{ + 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, }, - PersonalDataConsents: iabconsent.NoConsent, - MspaCoveredTransaction: iabconsent.MspaNotApplicable, - MspaOptOutOptionMode: iabconsent.MspaNotApplicable, - MspaServiceProviderMode: iabconsent.MspaNotApplicable, }, + // EU TCF V2, US Privacy, and US National MSPA + "DBACPeA~CPXuQIAPqd9YAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA.YAAAAAAAAAAA~1YYY~BEBAAQAAAkA.QA": { + iabconsent.SectionIDEUTCFv2: &iabconsent.V2ParsedConsent{ + Version: 2, + Created: time.Date(2022, time.April, 20, 0, 0, 0, 0, time.UTC), + LastUpdated: time.Date(2023, time.April, 19, 0, 0, 0, 0, time.UTC), + CMPID: 31, + CMPVersion: 640, + ConsentScreen: 1, + ConsentLanguage: "EN", + VendorListVersion: 126, + TCFPolicyVersion: 2, + IsServiceSpecific: true, + UseNonStandardStacks: false, + SpecialFeaturesOptIn: map[int]bool{}, + PurposesConsent: map[int]bool{}, + PurposesLITransparency: map[int]bool{}, + PurposeOneTreatment: false, + PublisherCC: "DE", + MaxConsentVendorID: 0, + IsConsentRangeEncoding: false, + ConsentedVendors: map[int]bool{}, + NumConsentEntries: 0, + ConsentedVendorsRange: []*iabconsent.RangeEntry(nil), + MaxInterestsVendorID: 0, + IsInterestsRangeEncoding: false, + InterestsVendors: map[int]bool{}, + NumInterestsEntries: 0, + InterestsVendorsRange: []*iabconsent.RangeEntry(nil), + NumPubRestrictions: 0, + PubRestrictionEntries: []*iabconsent.PubRestrictionEntry{}, + OOBDisclosedVendors: (*iabconsent.OOBVendorList)(nil), + OOBAllowedVendors: (*iabconsent.OOBVendorList)(nil), + PublisherTCEntry: &iabconsent.PublisherTCEntry{ + SegmentType: 3, + PubPurposesConsent: map[int]bool{}, + PubPurposesLITransparency: map[int]bool{}, + NumCustomPurposes: 0, + CustomPurposesConsent: map[int]bool{}, + CustomPurposesLITransparency: map[int]bool{}, + }, + }, + iabconsent.SectionIDUSPV1: &iabconsent.CcpaParsedConsent{ + Version: 1, + Notice: 'Y', + OptOutSale: 'Y', + LSPACoveredTransaction: 'Y', + }, + iabconsent.SectionIDUSNAT: &iabconsent.MspaParsedConsent{ + Version: 1, + SharingNotice: 0, + SaleOptOutNotice: 1, + SharingOptOutNotice: 0, + TargetedAdvertisingOptOutNotice: 0, + SensitiveDataProcessingOptOutNotice: 0, + SensitiveDataLimitUseNotice: 1, + SaleOptOut: 0, + SharingOptOut: 0, + TargetedAdvertisingOptOut: 0, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: 0, + 1: 0, + 2: 0, + 3: 1, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + 10: 0, + 11: 0, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: 0, + 1: 0, + }, + PersonalDataConsents: 0, + MspaCoveredTransaction: 2, + MspaOptOutOptionMode: 1, + MspaServiceProviderMode: 0, + Gpc: false, + }, }, } diff --git a/gpp_parsed_consent_test.go b/gpp_parsed_consent_test.go index 835f904..10eeaf3 100644 --- a/gpp_parsed_consent_test.go +++ b/gpp_parsed_consent_test.go @@ -144,6 +144,39 @@ func (s *MspaSuite) TestParseGppConsent(c *check.C) { } } +func (s *MspaSuite) TestMapGppSectionToParserMix(c *check.C) { + for gppString, expectedValues := range gppParsedMixedConsent { + 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) TestParseGppConsentMix(c *check.C) { + for g, e := range gppParsedMixedConsent { + 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 diff --git a/mspa_parsed_consent_fixture_test.go b/mspa_parsed_consent_fixture_test.go index 990f126..7ab980d 100644 --- a/mspa_parsed_consent_fixture_test.go +++ b/mspa_parsed_consent_fixture_test.go @@ -170,3 +170,169 @@ var usVAConsentFixtures = map[string]*iabconsent.MspaParsedConsent{ Gpc: false, }, } + +var usCAConsentFixtures = map[string]*iabconsent.MspaParsedConsent{ + "BVoAAACQ.QA": { + Version: 1, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: iabconsent.NoticeProvided, + SaleOptOut: iabconsent.NotOptedOut, + SharingOptOut: 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.ConsentNotApplicable, + 8: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + }, + PersonalDataConsents: iabconsent.ConsentNotApplicable, + MspaCoveredTransaction: iabconsent.MspaNo, + MspaOptOutOptionMode: iabconsent.MspaYes, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + + Gpc: false, + }, + "BVoBAARQ.QA": { + Version: 1, + SaleOptOutNotice: iabconsent.NoticeProvided, + SharingOptOutNotice: iabconsent.NoticeProvided, + SensitiveDataLimitUseNotice: 1, + SaleOptOut: 2, + SharingOptOut: 2, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.ConsentNotApplicable, + 2: iabconsent.ConsentNotApplicable, + 3: iabconsent.NoConsent, + 4: iabconsent.ConsentNotApplicable, + 5: iabconsent.ConsentNotApplicable, + 6: iabconsent.ConsentNotApplicable, + 7: iabconsent.ConsentNotApplicable, + 8: iabconsent.ConsentNotApplicable, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + 1: iabconsent.NoConsent, + }, + PersonalDataConsents: iabconsent.ConsentNotApplicable, + MspaCoveredTransaction: iabconsent.MspaYes, + MspaOptOutOptionMode: iabconsent.MspaYes, + MspaServiceProviderMode: iabconsent.MspaNotApplicable, + }, +} + +var usCOConsentFixtures = map[string]*iabconsent.MspaParsedConsent{ + "BAAAAAA.QA": { + Version: 1, + SharingNotice: 0, + SaleOptOutNotice: 0, + TargetedAdvertisingOptOutNotice: 0, + SaleOptOut: 0, + TargetedAdvertisingOptOut: 0, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + }, + MspaCoveredTransaction: 0, + MspaOptOutOptionMode: 0, + MspaServiceProviderMode: 0, + Gpc: false, + }, + "BAABAFA.QA": { + Version: 1, + SharingNotice: 0, + SaleOptOutNotice: 0, + TargetedAdvertisingOptOutNotice: 0, + SaleOptOut: 0, + TargetedAdvertisingOptOut: 0, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: 0, + 1: 0, + 2: 0, + 3: 1, + 4: 0, + 5: 0, + 6: 0, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: iabconsent.ConsentNotApplicable, + }, + MspaCoveredTransaction: 1, + MspaOptOutOptionMode: 1, + MspaServiceProviderMode: 0, + Gpc: false, + }, +} + +var usCTConsentFixtures = map[string]*iabconsent.MspaParsedConsent{ + "BBABAACQ.YA": { + Version: 1, + SharingNotice: 0, + SaleOptOutNotice: 0, + TargetedAdvertisingOptOutNotice: 1, + SaleOptOut: 0, + TargetedAdvertisingOptOut: 0, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: 0, + 1: 0, + 2: 0, + 3: 1, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{ + 0: 0, + 1: 0, + 2: 0, + }, + MspaCoveredTransaction: 0, + MspaOptOutOptionMode: 2, + MspaServiceProviderMode: 1, + Gpc: true, + }, +} + +var usUTConsentFixtures = map[string]*iabconsent.MspaParsedConsent{ + "BGBEQRkA": { + Version: 1, + SharingNotice: 0, + SaleOptOutNotice: 1, + TargetedAdvertisingOptOutNotice: 2, + SensitiveDataProcessingOptOutNotice: 0, + SaleOptOut: 0, + TargetedAdvertisingOptOut: 1, + SensitiveDataProcessing: map[int]iabconsent.MspaConsent{ + 0: 0, + 1: 1, + 2: 0, + 3: 1, + 4: 0, + 5: 0, + 6: 1, + 7: 0, + }, + KnownChildSensitiveDataConsents: map[int]iabconsent.MspaConsent{0: 1}, + MspaCoveredTransaction: 2, + MspaOptOutOptionMode: 1, + MspaServiceProviderMode: 0, + }, +} diff --git a/mspa_parsed_consent_test.go b/mspa_parsed_consent_test.go index 5d1a139..29986e7 100644 --- a/mspa_parsed_consent_test.go +++ b/mspa_parsed_consent_test.go @@ -183,3 +183,51 @@ func (s *MspaSuite) TestParseUsVAError(c *check.C) { c.Check(err, check.ErrorMatches, t.expected.Error()) } } + +func (s *MspaSuite) TestParseUsCA(c *check.C) { + for k, v := range usCAConsentFixtures { + c.Log(k) + + var gppSection = iabconsent.NewMspaCA(k) + var p, err = gppSection.ParseConsent() + + c.Check(err, check.IsNil) + c.Check(p, check.DeepEquals, v) + } +} + +func (s *MspaSuite) TestParseUsCO(c *check.C) { + for k, v := range usCOConsentFixtures { + c.Log(k) + + var gppSection = iabconsent.NewMspaCO(k) + var p, err = gppSection.ParseConsent() + + c.Check(err, check.IsNil) + c.Check(p, check.DeepEquals, v) + } +} + +func (s *MspaSuite) TestParseUsCT(c *check.C) { + for k, v := range usCTConsentFixtures { + c.Log(k) + + var gppSection = iabconsent.NewMspaCT(k) + var p, err = gppSection.ParseConsent() + + c.Check(err, check.IsNil) + c.Check(p, check.DeepEquals, v) + } +} + +func (s *MspaSuite) TestParseUsUT(c *check.C) { + for k, v := range usUTConsentFixtures { + c.Log(k) + + var gppSection = iabconsent.NewMspaUT(k) + var p, err = gppSection.ParseConsent() + + c.Check(err, check.IsNil) + c.Check(p, check.DeepEquals, v) + } +} diff --git a/mspa_sections.go b/mspa_sections.go index 7634955..61f01bf 100644 --- a/mspa_sections.go +++ b/mspa_sections.go @@ -15,12 +15,90 @@ type MspaUsVA struct { GppSection } +type MspaUsCA struct { + GppSection +} + +type MspaUsCO struct { + GppSection +} + +type MspaUsCT struct { + GppSection +} + +type MspaUsUT struct { + GppSection +} + +type TCFEU struct { + GppSection +} + +type TCFCA struct { + GppSection +} + +type USPV struct { + GppSection +} + +func NewTCFEU(section string) *TCFEU { + return &TCFEU{GppSection{sectionId: SectionIDEUTCFv2, sectionValue: section}} +} + +func NewTCFCA(section string) *TCFCA { + return &TCFCA{GppSection{sectionId: SectionIDCANTCF, sectionValue: section}} +} + +func NewUSPV(section string) *USPV { + return &USPV{GppSection{sectionId: SectionIDUSPV1, sectionValue: section}} +} + func NewMspaNational(section string) *MspaUsNational { - return &MspaUsNational{GppSection{sectionId: 7, sectionValue: section}} + return &MspaUsNational{GppSection{sectionId: SectionIDUSNAT, sectionValue: section}} +} + +func NewMspaCA(section string) *MspaUsCA { + return &MspaUsCA{GppSection{sectionId: SectionIDUSCA, sectionValue: section}} } func NewMspaVA(section string) *MspaUsVA { - return &MspaUsVA{GppSection{sectionId: 9, sectionValue: section}} + return &MspaUsVA{GppSection{sectionId: SectionIDUSVA, sectionValue: section}} +} + +func NewMspaCO(section string) *MspaUsCO { + return &MspaUsCO{GppSection{sectionId: SectionIDUSCO, sectionValue: section}} +} + +func NewMspaUT(section string) *MspaUsUT { + return &MspaUsUT{GppSection{sectionId: SectionIDUSUT, sectionValue: section}} +} + +func NewMspaCT(section string) *MspaUsCT { + return &MspaUsCT{GppSection{sectionId: SectionIDUSCT, sectionValue: section}} +} + +func (t TCFEU) ParseConsent() (GppParsedConsent, error) { + return ParseV2(t.sectionValue) +} + +func (t TCFCA) ParseConsent() (GppParsedConsent, error) { + return ParseV2(t.sectionValue) +} + +func (u USPV) ParseConsent() (GppParsedConsent, error) { + if valid, err := IsValidCCPAString(u.sectionValue); !valid { + return nil, err + } + + return &CcpaParsedConsent{ + int(u.sectionValue[0] - '0'), + u.sectionValue[1], + u.sectionValue[2], + u.sectionValue[3], + }, nil + } func (m *MspaUsNational) ParseConsent() (GppParsedConsent, error) { @@ -113,3 +191,165 @@ func (m *MspaUsVA) ParseConsent() (GppParsedConsent, error) { return p, r.Err } + +func (m MspaUsCO) 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 usva 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-States/CO + 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.TargetedAdvertisingOptOutNotice, _ = r.ReadMspaNotice() + p.SaleOptOut, _ = r.ReadMspaOptOut() + p.TargetedAdvertisingOptOut, _ = r.ReadMspaOptOut() + p.SensitiveDataProcessing, _ = r.ReadMspaBitfieldConsent(7) + p.KnownChildSensitiveDataConsents, _ = r.ReadMspaBitfieldConsent(1) + p.MspaCoveredTransaction, _ = r.ReadMspaNaYesNo() + 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 +} + +func (m MspaUsUT) 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 usva 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-States/UT + 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.TargetedAdvertisingOptOutNotice, _ = r.ReadMspaNotice() + p.SensitiveDataProcessingOptOutNotice, _ = r.ReadMspaNotice() + p.SaleOptOut, _ = r.ReadMspaOptOut() + p.TargetedAdvertisingOptOut, _ = r.ReadMspaOptOut() + p.SensitiveDataProcessing, _ = r.ReadMspaBitfieldConsent(8) + p.KnownChildSensitiveDataConsents, _ = r.ReadMspaBitfieldConsent(1) + p.MspaCoveredTransaction, _ = r.ReadMspaNaYesNo() + 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 +} + +func (m MspaUsCT) 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 usva 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-States/CT + 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.TargetedAdvertisingOptOutNotice, _ = r.ReadMspaNotice() + p.SaleOptOut, _ = r.ReadMspaOptOut() + p.TargetedAdvertisingOptOut, _ = r.ReadMspaOptOut() + p.SensitiveDataProcessing, _ = r.ReadMspaBitfieldConsent(8) + p.KnownChildSensitiveDataConsents, _ = r.ReadMspaBitfieldConsent(3) + p.MspaCoveredTransaction, _ = r.ReadMspaNaYesNo() + 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 + +} + +func (m MspaUsCA) 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 usva 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-States/CT + var p = &MspaParsedConsent{} + p.Version, _ = r.ReadInt(6) + + if p.Version != 1 { + return nil, errors.New("non-v1 string passed.") + } + + p.SaleOptOutNotice, _ = r.ReadMspaNotice() + p.SharingOptOutNotice, _ = r.ReadMspaNotice() + p.SensitiveDataLimitUseNotice, _ = r.ReadMspaNotice() + p.SaleOptOut, _ = r.ReadMspaOptOut() + p.SharingOptOut, _ = r.ReadMspaOptOut() + p.SensitiveDataProcessing, _ = r.ReadMspaBitfieldConsent(9) + p.KnownChildSensitiveDataConsents, _ = r.ReadMspaBitfieldConsent(2) + p.PersonalDataConsents, _ = r.ReadMspaConsent() + p.MspaCoveredTransaction, _ = r.ReadMspaNaYesNo() + 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 +}