delete suspended/deprovisioned users
macf0x opened this issue ยท 23 comments
Firstly, what a great extension. Awesome work.
Would it be possible to add a feature to delete users, based on some criteria?
Criteria could be suspended or deactivated.
ie. In people explorer ->suspended /deactivated -> (add a button) delete
Hi macf0x,
Depending on your criteria this can be achieved via Workflows / Automation in OKTA.
True, Workflows and Automation do cover a part of this, but no all use cases.
A casual look through the Okta ideas / feature requests, will reveal being able to delete users in a bulk fashion, either by criteria or manual bulk selection, has remain ignored. Presumably for philosophical reasons.
In our organisation we have several 100 Okta Tenants. Admin is distributed and each has a unique AD/LDAP env. Your extension tool in invaluable to these admins, but one area is adhoc bulk delete, that currently they can only perform per user.
rockstar doesn't really make changes.
here's a quick PowerShell script that uses my PowerShell module. it's only 15 lines
https://github.com/gabrielsroka/OktaAPI.psm1/blob/master/CallOktaAPI.ps1#L468-L482
@DCrawfordNHS , i'm not sure who you are, but thanks for your reply.
using the Official Python SDK:
https://github.com/okta/okta-sdk-python#pagination - pass in a filter to list_users()
https://github.com/okta/okta-sdk-python#remove-a-user
We already use the PS modules, but it requires API keys, which expire. Great for regular automation, but not so much adhoc tasks. It's a shame it's not possible to use OAuth with those modules.
The API keys expire if you don't use them for 30 days. Just have a job (or a calendar reminder) to renew them every 30 days.
OAuth
Someone would have to add that functionality. The PS modules were written before the Okta API supported OAuth.
Here's a Python example:
https://github.com/gabrielsroka/okta_api/blob/master/delete_users.py
Expiry is the exact issue we're trying to get around, which lead us to this feature request. But thanks for the info.
a little JavaScript you can run in browser, just like rockstar, it doesn't need an API token.
it doesn't paginate or observe rate limits -- exercise is left to the reader. [EDIT: see next comment for a version that paginates]
see also https://gabrielsroka.github.io/APIExplorer/
javascript:
/* Bookmark name: /Delete Users# */
(async function () {
if (!confirm('Delete Users?')) return;
const users = await $.getJSON('/api/v1/users?filter=status eq "SUSPENDED"'); /* DEPROVISIONED, SUSPENDED, etc. */
for (const u of users) {
console.log(u.profile.login, u.status);
if (u.status != 'DEPROVISIONED') {
await $.ajax(`/api/v1/users/${u.id}`, {method: 'delete'}); /* Must call delete twice. */
}
await $.ajax(`/api/v1/users/${u.id}`, {method: 'delete'});
}
})();
Cheers. That might just do the trick.
Thanks for providing this.
this version paginates, but it doesn't observe rate limits (though it might not need to)
you can run it as a Snippet or a Bookmarklet, see https://gabrielsroka.github.io/APIExplorer/
javascript:
/* Bookmark name: /Delete Users# */
(async function () {
if (!confirm('Delete Users?')) return;
var url = '/api/v1/users?filter=status eq "SUSPENDED"'; /* DEPROVISIONED, SUSPENDED, etc. */
while (url) {
const response = await fetch(url);
const users = await response.json();
for (const u of users) {
console.log(u.profile.login, u.status);
if (u.status != 'DEPROVISIONED') {
await $.ajax(`/api/v1/users/${u.id}`, {method: 'delete'}); /* Must call delete twice. */
}
await $.ajax(`/api/v1/users/${u.id}`, {method: 'delete'});
}
url = getNextUrl(response.headers.get('link'));
}
console.log('done');
function getNextUrl(linkHeader) {
const links = {};
linkHeader.split(', ').forEach(link => {
const [, url, name] = link.match(/<(.*)>; rel="(.*)"/);
links[name] = url;
});
if (links.next) {
const nextUrl = new URL(links.next); /* links.next is an absolute URL; we need a relative URL. */
return nextUrl.pathname + nextUrl.search;
}
}
})();
Just gave this a spin and it works great.
Had an exception on a user that was the technical contact, that was also suspended. Didn't want to remove it anyway. But is there a way to avoid that?
you might be able to add it to the filter
, or check the user's login/id in the code. i don't think there's an api to tell you who the technical contact is (tho u might be able to scrape the html) EDIT: see next comment
i was wrong. you can use /api/v1/org/contacts/technical
. it returns the userId
of the user
or, you can check for errors and recover gracefully
Nice I'll give that a shot.
@macf0x i found this old thread while looking for something else. right after my last post, i started working on a console: https://gabrielsroka.github.io/console for bits of code like this. i've rewritten the code above using the console. notice it's much shorter -- a lot of the fetch/pagination/rate-limit/etc. logic is in the console itself.
// Delete users using https://gabrielsroka.github.io/console
if (!confirm('Delete Users?')) return
url = '/api/v1/users?filter=status eq "SUSPENDED"' // DEPROVISIONED, SUSPENDED, etc.
for await (user of getObjects(url)) {
log('Deleting', user.profile.login, user.status)
if (user.status != 'DEPROVISIONED') {
await remove(`/api/v1/users/${user.id}`) // Must call remove() twice.
}
await remove(`/api/v1/users/${user.id}`)
if (cancel) {
log('Canceled.')
return
}
}
log('Done.')
Very nice. This will come in handy for adhoc tasks.
I was just thinking that a Expression Language tester would be a useful feature to add to your plug in. Especially when creating group rules, profile attribute mapping.
There's example code for this in the console
I'm guessing it's possible to evaluate EL (for group rules) without creating a group rule as the gui has a preview option.
I was more interested in the EL evaluation for attribute/profile mapping as there is no preview option for that.
If only we could see the Okta source, to see how there variant of SPEL works. :)
there is no preview
There is a preview, and i have code for that private api, too.
// Evaluate expression using https://gabrielsroka.github.io/console
// Set these:
body = {
sourceId: 'oty...',
targetId: 'oty...',
propertyMappings: [{
targetField: '...',
sourceExpression: '...'
}],
userId: '00u...'
}
prev = await postJson('/api/internal/v1/mappings/preview?includeAllTargetFields=false', body)
log(sourceExpression, '=>', prev.propertyMappings[0].error || prev.propertyMappings[0].value)
Yes you are correct. I was confused. The bit that doesn't have a preview is in the apps EL.
ie. Login/username and group attributes, (under the OIDC/SAML apps). The EL for OIDC claims can be previewed sort of, via the token preview, but you need to save. you can iterate through test conviently or quickly.
But this is awesome. Okta need to put you on the payroll :)
there are previews for that, too.
or, for SAML, use a SAML tracer. rockstar has one built in.
Is it really a preview if you need the save/change the config.