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 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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
|
||||||
);
|
);
|
||||||
|
@@ -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
|
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!
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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