feat: add role mappings for SAML configuration (#1210)

This commit is contained in:
Ömer Faruk Aydın
2023-08-11 19:07:39 +02:00
committed by GitHub
parent c7e1d30553
commit a6a124d2e6
8 changed files with 190 additions and 25 deletions

View File

@@ -0,0 +1,24 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(
'saml_auth_providers_role_mappings',
(table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table
.uuid('saml_auth_provider_id')
.references('id')
.inTable('saml_auth_providers');
table.uuid('role_id').references('id').inTable('roles');
table.string('remote_role_name').notNullable();
table.unique(['saml_auth_provider_id', 'remote_role_name']);
table.timestamps(true, true);
}
);
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('saml_auth_providers_role_mappings');
}

View File

@@ -26,6 +26,7 @@ import updateRole from './mutations/update-role.ee';
import updateStep from './mutations/update-step'; import updateStep from './mutations/update-step';
import updateUser from './mutations/update-user.ee'; import updateUser from './mutations/update-user.ee';
import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee'; import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee';
import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee';
import verifyConnection from './mutations/verify-connection'; import verifyConnection from './mutations/verify-connection';
const mutationResolvers = { const mutationResolvers = {
@@ -57,6 +58,7 @@ const mutationResolvers = {
updateStep, updateStep,
updateUser, updateUser,
upsertSamlAuthProvider, upsertSamlAuthProvider,
upsertSamlAuthProvidersRoleMappings,
verifyConnection, verifyConnection,
}; };

View File

@@ -33,17 +33,15 @@ const upsertSamlAuthProvider = async (
.limit(1) .limit(1)
.first(); .first();
let samlAuthProvider: SamlAuthProvider;
if (!existingSamlAuthProvider) { if (!existingSamlAuthProvider) {
samlAuthProvider = await SamlAuthProvider.query().insert( const samlAuthProvider = await SamlAuthProvider.query().insert(
samlAuthProviderPayload samlAuthProviderPayload
); );
return samlAuthProvider; return samlAuthProvider;
} }
samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById( const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
existingSamlAuthProvider.id, existingSamlAuthProvider.id,
samlAuthProviderPayload samlAuthProviderPayload
); );

View File

@@ -0,0 +1,54 @@
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import SamlAuthProvidersRoleMapping from '../../models/saml-auth-providers-role-mapping.ee';
import Context from '../../types/express/context';
type Params = {
input: {
samlAuthProviderId: string;
samlAuthProvidersRoleMappings: [
{
roleId: string;
remoteRoleName: string;
}
];
};
};
const upsertSamlAuthProvidersRoleMappings = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'SamlAuthProvider');
const samlAuthProviderId = params.input.samlAuthProviderId;
const samlAuthProvider = await SamlAuthProvider.query()
.findById(samlAuthProviderId)
.throwIfNotFound();
await samlAuthProvider
.$relatedQuery('samlAuthProvidersRoleMappings')
.delete();
if (!params.input.samlAuthProvidersRoleMappings) {
return [];
}
const samlAuthProvidersRoleMappingsData =
params.input.samlAuthProvidersRoleMappings.map(
(samlAuthProvidersRoleMapping) => ({
...samlAuthProvidersRoleMapping,
samlAuthProviderId: samlAuthProvider.id,
})
);
const samlAuthProvidersRoleMappings =
await SamlAuthProvidersRoleMapping.query().insert(
samlAuthProvidersRoleMappingsData
);
return samlAuthProvidersRoleMappings;
};
export default upsertSamlAuthProvidersRoleMappings;

View File

@@ -81,6 +81,9 @@ type Mutation {
updateStep(input: UpdateStepInput): Step updateStep(input: UpdateStepInput): Step
updateUser(input: UpdateUserInput): User updateUser(input: UpdateUserInput): User
upsertSamlAuthProvider(input: UpsertSamlAuthProviderInput): SamlAuthProvider upsertSamlAuthProvider(input: UpsertSamlAuthProviderInput): SamlAuthProvider
upsertSamlAuthProvidersRoleMappings(
input: UpsertSamlAuthProvidersRoleMappingsInput
): [SamlAuthProvidersRoleMapping]
verifyConnection(input: VerifyConnectionInput): Connection verifyConnection(input: VerifyConnectionInput): Connection
} }
@@ -307,6 +310,13 @@ type SamlAuthProvider {
active: Boolean active: Boolean
} }
type SamlAuthProvidersRoleMapping {
id: String
samlAuthProviderId: String
roleId: String
remoteRoleName: String
}
type UserConnection { type UserConnection {
edges: [UserEdge] edges: [UserEdge]
pageInfo: PageInfo pageInfo: PageInfo
@@ -352,6 +362,16 @@ input UpsertSamlAuthProviderInput {
active: Boolean! active: Boolean!
} }
input UpsertSamlAuthProvidersRoleMappingsInput {
samlAuthProviderId: String!
samlAuthProvidersRoleMappings: [SamlAuthProviderRoleMappingInput]
}
input SamlAuthProviderRoleMappingInput {
roleId: String!
remoteRoleName: String!
}
input DeleteConnectionInput { input DeleteConnectionInput {
id: String! id: String!
} }

View File

@@ -1,19 +1,27 @@
import SamlAuthProvider from '../models/saml-auth-provider.ee'; import SamlAuthProvider from '../models/saml-auth-provider.ee';
import User from '../models/user'; import User from '../models/user';
import Identity from '../models/identity.ee'; import Identity from '../models/identity.ee';
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
const getUser = (user: Record<string, unknown>, providerConfig: SamlAuthProvider) => ({ const getUser = (
user: Record<string, unknown>,
providerConfig: SamlAuthProvider
) => ({
name: user[providerConfig.firstnameAttributeName], name: user[providerConfig.firstnameAttributeName],
surname: user[providerConfig.surnameAttributeName], surname: user[providerConfig.surnameAttributeName],
id: user.nameID, id: user.nameID,
email: user[providerConfig.emailAttributeName], email: user[providerConfig.emailAttributeName],
role: user[providerConfig.roleAttributeName], role: user[providerConfig.roleAttributeName] as string | string[],
}) });
const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unknown>, samlAuthProvider: SamlAuthProvider) => { const findOrCreateUserBySamlIdentity = async (
userIdentity: Record<string, unknown>,
samlAuthProvider: SamlAuthProvider
) => {
const mappedUser = getUser(userIdentity, samlAuthProvider); const mappedUser = getUser(userIdentity, samlAuthProvider);
const identity = await Identity.query().findOne({ const identity = await Identity.query().findOne({
remote_id: mappedUser.id, remote_id: mappedUser.id,
provider_type: 'saml',
}); });
if (identity) { if (identity) {
@@ -22,25 +30,38 @@ const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unkno
return user; return user;
} }
const createdUser = await User.query().insertGraph({ const mappedRoles = Array.isArray(mappedUser.role)
fullName: [ ? mappedUser.role
mappedUser.name, : [mappedUser.role];
mappedUser.surname
] const samlAuthProviderRoleMapping = await samlAuthProvider
.filter(Boolean) .$relatedQuery('samlAuthProvidersRoleMappings')
.join(' '), .whereIn('remote_role_name', mappedRoles)
email: mappedUser.email as string, .limit(1)
roleId: samlAuthProvider.defaultRoleId, .first();
identities: [
const createdUser = await User.query()
.insertGraph(
{ {
remoteId: mappedUser.id as string, fullName: [mappedUser.name, mappedUser.surname]
providerId: samlAuthProvider.id, .filter(Boolean)
providerType: 'saml' .join(' '),
email: mappedUser.email as string,
roleId:
samlAuthProviderRoleMapping.roleId || samlAuthProvider.defaultRoleId,
identities: [
{
remoteId: mappedUser.id as string,
providerId: samlAuthProvider.id,
providerType: 'saml',
},
],
},
{
relate: ['identities'],
} }
] )
}, { .returning('*');
relate: ['identities']
}).returning('*');
return createdUser; return createdUser;
}; };

View File

@@ -3,6 +3,7 @@ import type { SamlConfig } from '@node-saml/passport-saml';
import appConfig from '../config/app'; import appConfig from '../config/app';
import Base from './base'; import Base from './base';
import Identity from './identity.ee'; import Identity from './identity.ee';
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee';
class SamlAuthProvider extends Base { class SamlAuthProvider extends Base {
id!: string; id!: string;
@@ -17,6 +18,7 @@ class SamlAuthProvider extends Base {
roleAttributeName: string; roleAttributeName: string;
defaultRoleId: string; defaultRoleId: string;
active: boolean; active: boolean;
samlAuthProvidersRoleMappings?: SamlAuthProvidersRoleMapping[];
static tableName = 'saml_auth_providers'; static tableName = 'saml_auth_providers';
@@ -63,6 +65,14 @@ class SamlAuthProvider extends Base {
to: 'saml_auth_providers.id', to: 'saml_auth_providers.id',
}, },
}, },
samlAuthProvidersRoleMappings: {
relation: Base.HasManyRelation,
modelClass: SamlAuthProvidersRoleMapping,
join: {
from: 'saml_auth_providers.id',
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
},
},
}); });
get config(): SamlConfig { get config(): SamlConfig {

View File

@@ -0,0 +1,36 @@
import Base from './base';
import SamlAuthProvider from './saml-auth-provider.ee';
class SamlAuthProvidersRoleMapping extends Base {
id!: string;
samlAuthProviderId: string;
roleId: string;
remoteRoleName: string;
static tableName = 'saml_auth_providers_role_mappings';
static jsonSchema = {
type: 'object',
required: ['samlAuthProviderId', 'roleId', 'remoteRoleName'],
properties: {
id: { type: 'string', format: 'uuid' },
samlAuthProviderId: { type: 'string', format: 'uuid' },
roleId: { type: 'string', format: 'uuid' },
remoteRoleName: { type: 'string', minLength: 1 },
},
};
static relationMappings = () => ({
samlAuthProvider: {
relation: Base.BelongsToOneRelation,
modelClass: SamlAuthProvider,
join: {
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
to: 'saml_auth_providers.id',
},
},
});
}
export default SamlAuthProvidersRoleMapping;