Sync Google Profile Pics

This is full middleware application that automates the process of syncing employee profile pictures between the HRMS (Workday) and the Email System (Google).

Here, I’ve used Python to create a SOAP request to a Workday Web Service. It downloads employees photos, resizes them, and then creates a request to Google AdminAPI to upload the photos – matching each one on the employee’s ID.

Feel free to use this code as is for your own organization, or as a base to build your own solution.

Setup:

This app requires your company to create a Workday report and enable it as web service. At a minimum the report should have Employee ID and the employee’s Photo as a Base64 text string. You’ll need to enter the endpoint for the service into the config file. This was created for a company who had two reports – one was a subset of employees (new hires) who they wanted to sync daily. The other contains all employees – this is the report you search to find an individual’s photo. If using one report, you would put the one endpoint in both endpoint locations in the config file.

In addition, you’ll find a requirements.txt file that shows you the python .venv environment I set up to run this app.

For security, the endpoints have been changed and the actual soap call has been removed. If you are customizing this app for your own company, you’ll need to create your own XML soap call to your own Workday endpoints. (SoapUI is free and excellent tool to do this). Store the SOAP requests as String variables.

And finally, you will need a Google Workspace corporate account, administrative access, and the ability to set up an API Project on the account.

For the full application, please see the zip file. This description leaves out some details you might need if you trying to recreate this application.

Link to the FULL CODE

Soap Request to Workday

The Soap Request to Workday is broken out into its own class called ahSoapSender. py. It has a function that calls the Workday Report and returns a list of all the Employee Numbers in the report. (This allows the company to scope the application. For instance, they may only want to sync new employees). This function uses python Requests to create a request, sending the xml soap request in as the data paramenter.

Here, “body” is the String Variable containing the SOAP request you created.

body = getXMLSoapRequest()
listImGoingToReturn = []
try:
    response = requests.post(self._endpoint, data=body,   headers=self._headers)
    log.info("the reponse's status code is {}".format(response.status_code))
    if(response.status_code != '200' and response.status_code != 200):
         log.critical("Workday response : {}".format(response.text))
except Exception as e : 
     ...
return listImGoingToReturn

Because it’s Workday, the response is returned as XML, so I use xmltodict to parse it to a dictionary so I can work with it more easily.

asdict = xmltodict.parse((response.content).decode('utf-8'))
allEmployees = asdict['env:Envelope']['env:Body']['wd:Report_Data']['wd:Report_Entry']

If you are doing this from scratch, the best way to figure out where in the dictionary your employee ID is to print out a single example and parse through each level. I use notepad++. Once you know where to find the employee ID, check each record in your dictionary at that level see if this value exists. If it does, add it to the list.

for emp in allEmployees:
   if 'wd:EMPLOYEEID' in emp:
       employeeid = emp['wd:EMPLOYEEID']
           
#if they have an employee ID, put it in the list
           listImGoingToReturn.append(employeeid)
    else: 
       log.info("can't get picture for a record because they don't have an employee ID in workday")
       

Now that you have created a list of Employee IDs you want to sync, you can go through the list and for each ID, you want to connect to Workday to pull the photo, then connect Google to put the photo.

Transferring from one system to the other

if(len(listOfEmployees)>0):
        log.info("Workday returned a list of {} employees.".format(len(listOfEmployees)))
        
        for guy in listOfEmployees:
            #look up a photo 
                guy = ahSS.getPerson(guy) 
            #put the photo in Google
                try:
                    ahg.updatePhoto(guy.email,guy.photo)
                    log.info(f"Updated photo on Google for {guy.email}")
                except Exception as err:
                    log.critical("Failed at ~program line 63ish: {}".format(err))
        
    

The function to get the photo from Workday is very similar to getting the employee ID from Workday which we just covered, so instead we’ll focus on getting the photo into Google now.

Putting the Photos in Google

Firstly, this discussion isn’t complete without explaining how to set this up on the Google side.

  1. You need to have created a project and a service account for that project.
  2. The service account needs to have been granted token granting permission in your IAM console.
  3. You need have created credentials for the service account and downloaded them as a .json file.
  4. You need to have granted the following scopes to this service account in your admin settings:
    • https://www.googleapis.com/auth/gmail.send
    • https://www.googleapis.com/auth/admin.directory.user

Once that is all set up you should be good to go with the code. All of the code for the handling the Google upload in this app is in the ahGoogle.py class.

Next – You need to handle OAuth authentication and get your token. I’ve created a handy re-usable function for this:

def auth(self):
        creds = None
    
        credentials = service_account.Credentials.from_service_account_file(self.SERVICE_ACCOUNT_FILE, scopes=self.SCOPES)
        self._credentials = credentials

Now, there are several different ways to authenticate to Google, but we want to use a way that doesn’t prompt for human intervention, so we need to use a service account, but the service account will need to use delegated credentials of an actual domain admin:

 creds = self._credentials
        
        delegated_credentials = creds.with_subject('admin@yourdomain.com')
service = build('admin','directory_v1', credentials=delegated_credentials)

Next, we need to construct the payload of our request. We’re going to select the Google Service.

Then we’re going to change the illegal characters in the base64img we got from Workday (Important Note: Workday returns a technically URL Safe base64 image string, but it’s not URL-safe-enough for Google, which has stricter rules about certain characters).

After that we’re going to create a JSON object that includes all the data we want to pass in. The structure of this object is very specific and is detailed in the documentation for the Google API.

Here’s what all that looks like:

service = build('admin','directory_v1', credentials=delegated_credentials)
        image = photo.replace("/","_")
        image = image.replace("+","-")
        image = image.replace("=","")
        userPhoto = {
            'primaryEmail': pmail,
            'kind': 'admin#directory#user#photo',
            #'photoData': str(byteImg),
            'photoData': image,
            'mimeType': 'JPEG'
        }

And finally, we send it to Google:

 results = service.users().photos().update(userKey='{}'.format(pmail), body=userPhoto)
        
        log.info(results)

Use the automation scheduling tool of your choice – the simplest and cheapest solution is to use Windows Task Scheduler to launch the main class (program.py) at whatever daily or hourly timing schedule works best for your business process.