profgriswald
profgriswald2mo ago

Can't set user grants from info in user metadata set from external auth provider via Actions

Hey all. I might be missing something obvious here, but hope someone can point me in the right direction. I'm self-hosting on v3.3.0 and trying to set up a user access flow using ActionsV1 and GitHub as the identity provider (via Dex) and seem to have hit a wall due to the availability of certain methods in Actions and their execution flow. This is a flow I had set up in Keycloak which, whilst pretty clunky, was working as expected. I'm running Zitadel as an internal service to development teams. The flow I'm trying to set up is the following: 1. Internal user without an existing Zitadel account wants to access one of the clients, or accesses the Zitadel UI directly. 2. Zitadel prompts for login, user selects to sign-in via GitHub 3. User authenticates successfully and gets redirected back to Zitadel 4. User is presented with the few profile fields to fill out and clicks "Register" to create their Zitadel user 5. Based on the GitHub teams list returned by Dex as part of 3, add user metadata to their Zitadel user containing a "primary" team and the full list of GitHub teams they're a member of 6. Automatically assign user grants for that user based on their team membership as defined in user metadata I currently have two Actions configured: processGitHubLogin, which runs on the ExternalAuth flow at PostAuth, and grantInitialRoles which also runs on the ExternalAuth flow at PostCreation stage.
6 Replies
profgriswald
profgriswaldOP2mo ago
In processGitHubLogin, I'm getting the list of GitHub org team memberships back from the Dex login flow and then assigning those as user metadata like so:
function processGitHubLogin(ctx, api) {
const log = require('zitadel/log');

if (ctx.v1.externalUser?.externalIdpId != "${idp_id}" || !ctx.v1.externalUser) {
return;
}

const groups = ctx.getClaim("groups");

const orgTeams = // filter teams from groups claim

const teamMappings = {
// team name => short name mappings
};

let primaryTeam = 'engineering';

for (const team of Object.keys(teamMappings)) {
if (orgTeams.includes(team)) {
primaryTeam = teamMappings[team] || 'engineering';
break;
}
}

api.v1.user.appendMetadata('team', primaryTeam);
api.v1.user.appendMetadata('github_teams', orgTeams.join(','));
api.v1.user.appendMetadata('github_username', ctx.getClaim('preferred_username') || '');

log.info('GitHub team mapping', JSON.stringify({
userId: ctx.v1.externalUser.externalId,
githubUser: ctx.getClaim("preferred_username"),
teams: orgTeams,
primaryTeam: primaryTeam
}));
}
function processGitHubLogin(ctx, api) {
const log = require('zitadel/log');

if (ctx.v1.externalUser?.externalIdpId != "${idp_id}" || !ctx.v1.externalUser) {
return;
}

const groups = ctx.getClaim("groups");

const orgTeams = // filter teams from groups claim

const teamMappings = {
// team name => short name mappings
};

let primaryTeam = 'engineering';

for (const team of Object.keys(teamMappings)) {
if (orgTeams.includes(team)) {
primaryTeam = teamMappings[team] || 'engineering';
break;
}
}

api.v1.user.appendMetadata('team', primaryTeam);
api.v1.user.appendMetadata('github_teams', orgTeams.join(','));
api.v1.user.appendMetadata('github_username', ctx.getClaim('preferred_username') || '');

log.info('GitHub team mapping', JSON.stringify({
userId: ctx.v1.externalUser.externalId,
githubUser: ctx.getClaim("preferred_username"),
teams: orgTeams,
primaryTeam: primaryTeam
}));
}
And then in grantInitialRoles I'm trying to set user grants based on that stored user metadata:
function grantInitialRoles(ctx, api) {
const log = require('zitadel/log');
const team = ctx.v1.user?.getMetadata("team");
log.info('team: ', team);

if (!team) { return; }

const projects = {
// mapping of project names to IDs
};

api.v1.appendUserGrant({
// single shared user grant for all users
});

if (team === "platform") {
for (const project in Object.keys(projects)) {
log.info(project);
api.v1.appendUserGrant({
projectId: projects[project],
roleKeys: [`$${project}:admin`],
});
}
}
}
function grantInitialRoles(ctx, api) {
const log = require('zitadel/log');
const team = ctx.v1.user?.getMetadata("team");
log.info('team: ', team);

if (!team) { return; }

const projects = {
// mapping of project names to IDs
};

api.v1.appendUserGrant({
// single shared user grant for all users
});

if (team === "platform") {
for (const project in Object.keys(projects)) {
log.info(project);
api.v1.appendUserGrant({
projectId: projects[project],
roleKeys: [`$${project}:admin`],
});
}
}
}
I'm facing a few problems here though: 1. On initial user creation and the execution of the processGitHubLogin action, no metadata is appended until the second time that user authenticates via GitHub. The console log in the action is printed the correct data, but it can't set metadata since the user doesn't exist in Zitadel at this point. 2. Given this, grantInitialRoles doesn't work. Since it's being executed at PostCreation phase (and only the once, since the trigger runs at the completion of registration), and since the user metadata doesn't exist, I can't set the grants. As far as I can find, appendUserGrant is only available at PostCreation for InternalAuth and ExternalAuth flows, so I'm not sure how I can set these user grants up through any auth flow. I've tried creating the internal user beforehand and then linking to the GitHub auth user, which does correctly set the user metadata but obviously the user grant Action won't run since the user was already created. Any advice would be great, as I'm fresh out of ideas! The only other approach I've considered here is having a service user set up and making API calls in the Actions to assign grants instead, but I don't want to go down that route unless there aren't any alternatives that I might be missing. (though I might have to if I want to assign instance roles based on team membership since I don't think that's supported in Actions?) Passing thought, though I haven’t tried it yet, but would moving the first action from PostAuth to PreCreation make any difference? I’m assuming not since the user still won’t have been created yet.
profgriswald
profgriswaldOP2mo ago
Just came across https://github.com/zitadel/zitadel/issues/8497 which would’ve solved this without necessitating migration to Actions V2, which I’m not against but the lack of support for V2 in the TF provider at the moment is a massive adoption barrier.
GitHub
Actions: appendUserGrant on post authentication triggers · Issue #...
Action currently allow to appendUserGrant on the post creation trigger. This works fine for the registration / first time provisioned users, but lacks the ability to update user grants on already c...
Rajat
Rajat2mo ago
hey @profgriswald good day, indeed but actions v2 are still very much wip, I will let you know when we plan t release the TF provider for it
profgriswald
profgriswaldOP2mo ago
Hey @Rajat, indeed! Very much understood. I'd rather avoid any sizeable migrations if at all possible anyhow 🙂
Rajat
Rajat2mo ago
hey @profgriswald can you try processGitHubLogin and set it as ExternalAuth → Pre Creation instead of Post Auth and try the flow again. It also allows metadata setting before the user is created. Can you try it?. Also I am hoping that the method name and the action name is same. MOre about it on actions doc
profgriswald
profgriswaldOP2mo ago
@Rajat Sure let me try that now. And yes the function and action are the same name. @Rajat I'm not entirely sure how this approach will work, since the externalUser object returned by the external auth is only available in the Post Auth trigger. Actually that's not strictly true, I can see it's there under ctx.v1.authRequest.linkingUsers. Now seeing if I can get this hooked up. Yeah, the ID token from the external auth is only in the Post Auth trigger, and not in the Pre or Post Creation triggers, so there's no way for me to get the claims returned by the external auth Just to circle back round to this, two points to mention: 1. With assigning metadata, I managed to get this working with appendMetadata calls in the Post Auth action, but those calls only seemed to work AFTER calls to supporting methods like setFirstName, possibly because there was no metadata to append to otherwise? 2. I ended up working around granting roles in a Post Creation action albeit in a slightly awkward way (which will likely be moot with Actions V2) by creating a service user and fetching an oauth token in the action and then using that to grab the user metadata and call appendUserGrant depending on that metadata

Did you find this page helpful?