feat: add role mappings for SAML configuration (#1210)
This commit is contained in:
@@ -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');
|
||||
}
|
@@ -26,6 +26,7 @@ import updateRole from './mutations/update-role.ee';
|
||||
import updateStep from './mutations/update-step';
|
||||
import updateUser from './mutations/update-user.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';
|
||||
|
||||
const mutationResolvers = {
|
||||
@@ -57,6 +58,7 @@ const mutationResolvers = {
|
||||
updateStep,
|
||||
updateUser,
|
||||
upsertSamlAuthProvider,
|
||||
upsertSamlAuthProvidersRoleMappings,
|
||||
verifyConnection,
|
||||
};
|
||||
|
||||
|
@@ -33,17 +33,15 @@ const upsertSamlAuthProvider = async (
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
let samlAuthProvider: SamlAuthProvider;
|
||||
|
||||
if (!existingSamlAuthProvider) {
|
||||
samlAuthProvider = await SamlAuthProvider.query().insert(
|
||||
const samlAuthProvider = await SamlAuthProvider.query().insert(
|
||||
samlAuthProviderPayload
|
||||
);
|
||||
|
||||
return samlAuthProvider;
|
||||
}
|
||||
|
||||
samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
|
||||
const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
|
||||
existingSamlAuthProvider.id,
|
||||
samlAuthProviderPayload
|
||||
);
|
||||
|
@@ -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;
|
@@ -81,6 +81,9 @@ type Mutation {
|
||||
updateStep(input: UpdateStepInput): Step
|
||||
updateUser(input: UpdateUserInput): User
|
||||
upsertSamlAuthProvider(input: UpsertSamlAuthProviderInput): SamlAuthProvider
|
||||
upsertSamlAuthProvidersRoleMappings(
|
||||
input: UpsertSamlAuthProvidersRoleMappingsInput
|
||||
): [SamlAuthProvidersRoleMapping]
|
||||
verifyConnection(input: VerifyConnectionInput): Connection
|
||||
}
|
||||
|
||||
@@ -307,6 +310,13 @@ type SamlAuthProvider {
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
type SamlAuthProvidersRoleMapping {
|
||||
id: String
|
||||
samlAuthProviderId: String
|
||||
roleId: String
|
||||
remoteRoleName: String
|
||||
}
|
||||
|
||||
type UserConnection {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
@@ -352,6 +362,16 @@ input UpsertSamlAuthProviderInput {
|
||||
active: Boolean!
|
||||
}
|
||||
|
||||
input UpsertSamlAuthProvidersRoleMappingsInput {
|
||||
samlAuthProviderId: String!
|
||||
samlAuthProvidersRoleMappings: [SamlAuthProviderRoleMappingInput]
|
||||
}
|
||||
|
||||
input SamlAuthProviderRoleMappingInput {
|
||||
roleId: String!
|
||||
remoteRoleName: String!
|
||||
}
|
||||
|
||||
input DeleteConnectionInput {
|
||||
id: String!
|
||||
}
|
||||
|
@@ -1,19 +1,27 @@
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import User from '../models/user';
|
||||
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],
|
||||
surname: user[providerConfig.surnameAttributeName],
|
||||
id: user.nameID,
|
||||
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 identity = await Identity.query().findOne({
|
||||
remote_id: mappedUser.id,
|
||||
provider_type: 'saml',
|
||||
});
|
||||
|
||||
if (identity) {
|
||||
@@ -22,25 +30,38 @@ const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unkno
|
||||
return user;
|
||||
}
|
||||
|
||||
const createdUser = await User.query().insertGraph({
|
||||
fullName: [
|
||||
mappedUser.name,
|
||||
mappedUser.surname
|
||||
]
|
||||
const mappedRoles = Array.isArray(mappedUser.role)
|
||||
? mappedUser.role
|
||||
: [mappedUser.role];
|
||||
|
||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.whereIn('remote_role_name', mappedRoles)
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
const createdUser = await User.query()
|
||||
.insertGraph(
|
||||
{
|
||||
fullName: [mappedUser.name, mappedUser.surname]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
email: mappedUser.email as string,
|
||||
roleId: samlAuthProvider.defaultRoleId,
|
||||
roleId:
|
||||
samlAuthProviderRoleMapping.roleId || samlAuthProvider.defaultRoleId,
|
||||
identities: [
|
||||
{
|
||||
remoteId: mappedUser.id as string,
|
||||
providerId: samlAuthProvider.id,
|
||||
providerType: 'saml'
|
||||
providerType: 'saml',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
relate: ['identities'],
|
||||
}
|
||||
]
|
||||
}, {
|
||||
relate: ['identities']
|
||||
}).returning('*');
|
||||
)
|
||||
.returning('*');
|
||||
|
||||
return createdUser;
|
||||
};
|
||||
|
@@ -3,6 +3,7 @@ import type { SamlConfig } from '@node-saml/passport-saml';
|
||||
import appConfig from '../config/app';
|
||||
import Base from './base';
|
||||
import Identity from './identity.ee';
|
||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee';
|
||||
|
||||
class SamlAuthProvider extends Base {
|
||||
id!: string;
|
||||
@@ -17,6 +18,7 @@ class SamlAuthProvider extends Base {
|
||||
roleAttributeName: string;
|
||||
defaultRoleId: string;
|
||||
active: boolean;
|
||||
samlAuthProvidersRoleMappings?: SamlAuthProvidersRoleMapping[];
|
||||
|
||||
static tableName = 'saml_auth_providers';
|
||||
|
||||
@@ -63,6 +65,14 @@ class SamlAuthProvider extends Base {
|
||||
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 {
|
||||
|
@@ -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;
|
Reference in New Issue
Block a user