A few months ago, we introduced an intuitive feature to Sym: the ability to select a Slack user (or multiple Slack users) in a dropdown as part of your access request form. This makes it unnecessary to rely on clunkier solutions like manually typing in a user’s email address or username, which is naturally error-prone. While the feature itself is easy to describe and understand, the decisions our team made as we created it represent our philosophy on making access management workflows easy to implement while minimizing boilerplate and toil. Also, as with most easy-to-describe features, this effort had some real hidden complexity under the hood. In this post, I’ll walk through the feature itself and some of the decision-making our team made in designing it.
Simply put, an implementer of the Sym platform should, to whatever extent possible, be enabled to think about their solution not in terms of the code they’re writing, but of the problem they’re solving. Minutia, boilerplate, and “non-working” code should be minimized; expressions of logic that impact a workflow should be enabled to be clear and legible; the translation from a business process diagram to code should feel obvious and intuitive.
There are ways to situationally express this philosophy in short-hand (a current favorite is “principle of least surprise”), but the goal always boils down to the same thing: the implementer should be thinking about their problems – not ours.
With this in mind, let’s take a look at the journey from a naive solution, to what we believe is a best-case solution.
Let’s look at a common Sym use case we have observed and, in fact, use ourselves internally: naming a specific person in your organization to approve your request. Before this feature, you would need to do something like have a free-form text box for the user’s email address in the request form, and then use that address input to send a DM from the Sym SDK. But then you run into two problems: You need to make sure the user has entered a valid email address in your organization, and you would need to make sure the user didn’t enter their own email address. The Python code you might have written to make this happen in Sym would look something like this:
It’s not a great experience for the user or the implementer: not only does the requester have to manually type a full email address into their request, but the code itself is complex, hard to parse, and therefore error prone. Granted, this is still simpler than if you weren’t using Sym, since then you would have to define all the helper methods used here, like slack.list_users(), but it’s still far from ideal!
This is where the Slack user selection dropdown comes in. It’s a standard block type supported by Slack where they pre-populate a dropdown with all the users in your workspace. Done and done, right?
Of course not – that’s only half the problem. While Slack guarantees that you have selected a valid user in your Slack workspace, all they give you back is the selected user’s Slack user ID – something like U1234ABC56. This is a good step towards reducing complexity, but still doesn’t create a great experience because it’s, well, just an ID. Let’s update the example from above:
Now, this is overall quite a bit better and perfectly functional, but it isn’t the delightful experience we want to create. Put simply, IDs are not something we want an implementer to think about, ever – our design philosophy dictates that in this situation, a user is a user, and should be handled as a user. (And one part, the on_approve() hook that determines who is allowed to approve the request, actually got a bit worse, which is another strike against this flavor of solution).
What we really want is this:
Straightforward and effective. This is what we ended up creating, and there’s a lot more to this feature than merely supporting Slack’s user selection dropdown blocks.
Sym abstracts away much of the complexity around users and identity: here, we’ve shown that there’s no need to make manual calls to the Slack API to get information about the selected user, which is handy. That said, with a shared abstraction for user identity, it’s also easy to tie this into another service, like Okta.
Let’s extend the example to ensure that the selected user is in a specific Okta group:
No need to do manual lookups, or to figure out how to turn a Slack user ID into an email, then into an Okta user ID; that all gets handled for you and exemplifies one of the overarching goals of Sym—to provide a tool for engineers to make it easy to express complex just-in-time access rules as simple, succinct code.
Of course, we do a lot in the background to make all this work – an ergonomic, “least surprising” SDK is always going to hide some complexity that has to be dealt with somewhere. In this particular case, the major complexity was latency, which led to a service refactor in how we manage Slack identity lookups.
Previously we’ve been able to make the assumption that an individual access request is essentially “complete” (from a data modeling perspective) once a user submits it; it has all the information necessary for us to figure out what the user needs access to, who needs to review the request, and what information we need to make available in our SDK. Since Slack doesn’t provide the details of selected users though, just their user IDs, this is no longer the case: we need to make additional API calls or lookups in our database to retrieve the necessary information.
A common constraint that we (and other apps in Slack) run into is that Slack requires a response to interactions within ~3 seconds of the user submitting the interaction, so there were some minor architectural changes needed to Sym to support doing the Slack user lookups required to make this feature magical without causing the user to see request timeouts. We also needed to make sure we wouldn’t blow through Slack’s API rate limits.
Given that we don’t restrict the number of users that can be selected in a dropdown, this means that we need to do this asynchronously to avoid exceeding the Slack interaction timeout, which in turn meant that we needed to move this out of our API service (which handles Slack event interactions) and into our queueing service, which also handles pre-processing of Sym events.
In our queuing service, we attempt to match the selected Slack user IDs to user information we already have on hand in the Sym user database, or if we can’t find any we call the Slack API to get the user profile (and then create a user in the Sym user database, as part of Sym’s automatic identity management system). This information is then persisted in a per-request store, effectively caching the information for the lifetime of that single request so we don’t have to do these lookups multiple times per request. From there, the request is sent along to our runtime service, which is responsible for actually executing the workflows our customers specify, including the customer-provided Python implementation code. Before executing any such customer-provided code, our runtime service finally translates any Slack user IDs in the event payload to the user objects retrieved from the per-request store, enabling customers to work with those objects instead of IDs in their workflows.
We also had the ever-present consideration of designing our API methods to “rhyme” with each other: getting a user in Slack should feel like getting a user in Okta (or AWS IAM, or OneLogin, or PagerDuty).
We’re happy with where we ended up: an intuitive, performant typeahead field that translates in the Sym SDK into an obvious, useful object. Easy to describe, kind of complex to build, and ultimately, useful for Sym customers.
Want to learn more? Check out our documentation for the feature!