Skip to content

Conversation

asloobq
Copy link
Contributor

@asloobq asloobq commented Mar 19, 2024

Introduce two new clients BidStreamClient and SharingClient and deprecate the existing Uid2PublisherClient.

  1. Accept parameter domain_name for decrypting ad tokens, but allow None for now. The actual domain_name checks will follow in a future PR
  2. Change encrypt / decrypt / refresh interface to return a *response object always that contains the value or exception reason. Keeps it consistent with C# SDK
  3. Checks if bidstream tokens and sharing tokens have a valid lifetime as returned from operator
  4. Return expiry_time in response if decryption fails with TOKEN_EXPIRED error
  5. Returns if decrypted token was client side generated and whether its email/phone
  6. Fixed a bug where created_at was being set to 1 hr earlier
  7. Included a fix from https://github.com/IABTechLab/uid2-client-python/pull/10/files to generate consistent prefix

@@ -116,25 +114,7 @@ def decrypt(self, token):
EncryptionError: if token version is not supported, the token has expired,
or no required decryption keys present in the keys collection
"""
return encryption.decrypt(token, self._keys)

def _parse_keys_json(self, resp_body):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to refresh_keys_util.py

@@ -79,15 +80,12 @@ def refresh_keys(self):
Returns:
EncryptionKeysCollection containing the keys
"""
req, nonce = make_v2_request(self._secret_key, dt.datetime.now(tz=timezone.utc))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to refresh_keys_util.py

@asloobq asloobq marked this pull request as ready for review March 26, 2024 00:34
@@ -36,7 +38,7 @@ class Uid2Client:
Connect to the UID2 service and obtain the latest encryption keys:
>>> from uid2_client import *
>>> client = Uid2Client('https://prod.uidapi.com', 'my-authorization-key', 'my-secret-key')
>>> keys = client.refresh_keys()
>>> keys = client.refresh_sharing_keys()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should remain unchanged

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, I missed this

self._auth_key = auth_key
self._secret_key = base64.b64decode(secret_key)

def encrypt_raw_uid_into_sharing_token(self, uid2, keyset_id=None, now=dt.datetime.now(tz=dt.timezone.utc)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we don't want to have a now parameter on these public methods. In the .NET SDK the DateTime param is on the internal method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. I missed that the C# method is internal. Changed the method signature

self._auth_key = auth_key
self._secret_key = base64.b64decode(secret_key)

def encrypt_raw_uid_into_sharing_token(self, uid2, keyset_id=None, now=dt.datetime.now(tz=dt.timezone.utc)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default argument values are evaluated at the point of function definition, so now doesn't behave the way we want it to. This is a per-existing issue but we should fix it, at least in new code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very interesting. I didn't know this and assumed they are evaluated when the function is invoked. Thanks!

@@ -1,7 +1,4 @@
"""Internal module for holding the Uid2Client class.

Do not use this module directly, import through uid2_client module instead, e.g.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this comment removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UID2Client class is being deprecated and participants can use this directly. The publisher guide mentions this

base64.b64decode(key['secret']),
keyset_id)
keys.append(key)
if resp_body["identity_scope"] == "EUID":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this inside the keys loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's right, it doesn't need to be in the for loop. My indentation is incorrect

Comment on lines 18 to 20
keyset_id = None
if "keyset_id" in key:
keyset_id = key["keyset_id"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do

keyset_id = key.get("keyset_id")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's more elegant

pyproject.toml Outdated
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "uid2_client"
version = "2.2.8"
version = "2.3.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, when do we need to change the version?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cYKatherine @thomasm-ttd is this handled by the github action now, so we shouldn't change version manually like this?

if resp_body["identity_scope"] == "EUID":
identity_scope = IdentityScope.EUID
for key in resp_body["keys"]:
keyset_id = key.get("keyset_id")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVM, this function is also used in the deprecated client class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its being removed from /key/bidstream response but it will continue to exist in /key/sharing response

keyset_id = key.get("keyset_id")
key = EncryptionKey(key['id'],
key.get('site_id', -1),
_make_dt(key['created']),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like to replace all method to get(), like key.get("created")?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for response fields which are necessary for the key to work, it makes sense to alert the user in case of a missing value. So key['created'] will throw an exception if the field is absent.
Using key.get("created") it will silently fail. I am assuming that alerting the user by throwing an exception is the right way to go

self.assertEqual(result.identity_scope, expected_scope)
self.assertEqual(result.advertising_token_version, expected_version)

def test_phone_uids(self, mock_refresh_sharing_keys): # PhoneTest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the PhoneTest equivalent? Let's use the same order between SDKs to make it easier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually did reorganize the test cases between the SDKs :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate? We need to keep them in the same order to make it easier for future maintainers. If you want to propose a better order, let's discuss but you'll need to change them in all SDKs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just meant that I sync'd the tests in python SDK with the C# one. They were not in the same order in Python SDK.

print('UID =', decrypt_result.uid)
print('Established =', decrypt_result.established)
print('Site ID =', decrypt_result.site_id)
print('Identity Scope =', decrypt_result.identity_scope)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

identity_scope belongs at the BidstreamClient level rather than the token level. It's probably not needed there either since it's the consumer who chooses whether to connect to EUID vs UID2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. will remove this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems you've still left some identity_scopes at the token level, could you please check?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named bidstream_client.py?



class BidstreamClient:
"""Client for interacting with UID2 BidStream services
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/BidStream/Bidstream/


Methods:
refresh_keys: Refresh encryption keys from UID2 servers
decrypt_ad_token_into_raw_uid: decrypt an advertising token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decrypt_token_into_raw_uid

"""

def __init__(self, base_url, auth_key, secret_key):
"""Create a new BidStreamClient client.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BidstreamClient

@@ -32,8 +38,9 @@ class _PayloadType(Enum):
base64_url_special_chars = {"-", "_"}


# DEPRECATED, DO NOT CALL DIRECTLY. PLEASE USE Uid2Client's client.decrypt()
def decrypt(token, keys, now=dt.datetime.now(tz=timezone.utc)):
# DEPRECATED, DO NOT CALL DIRECTLY. For DSPs PLEASE USE BidStreamClient's decrypt_ad_token_into_raw_uid()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bidstream / decrypt_token_into_raw_uid


This will synchronously connect to the corresponding UID2 service and fetch the latest
set of encryption keys which can then be used to decrypt advertising tokens using
the decrypt_token function.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decrypt_token_into_raw_uid



class ClientType(Enum):
Sharing = 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make these UPPER_CASE to match DecryptionStatus enum values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I copied it from C# SDk and forgot to change

@property
def encrypted_data(self):
return self._encrypted_data
#uid = encrypted_data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete this line

@@ -24,14 +27,14 @@ def _usage():
# client = EuidClientFactory.create(base_url, auth_key, secret_key)
# for UID2 use:
client = Uid2ClientFactory.create(base_url, auth_key, secret_key)
client.refresh_keys()
client.refresh()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this still called refresh_keys?

established (datetime): UTC date/time for when the token was first generated
site_id (int): site ID which the token is originating from
site_key_site_id (int): site ID of the site key which the token is encrypted with
"""

def __init__(self, uid2, established, site_id, site_key_site_id):
self.uid2 = uid2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes this release a breaking change, so we need to bump the major version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I was trying not to break any existing functionality, for now I added a property alias uid2 for existing clients to work. At the same time the overall changes in the PR look big enough to me to warrant a major version upgrade.

@asloobq asloobq force-pushed the aaq-UID2-2877-dsp-check-client-refactor branch from 89738e5 to 588e980 Compare April 8, 2024 22:06
@asloobq asloobq merged commit f1329a4 into main Apr 10, 2024
@asloobq asloobq deleted the aaq-UID2-2877-dsp-check-client-refactor branch April 10, 2024 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants