Просмотр исходного кода

Merge pull request #7761 from weseek/feat/gw-7958-update-authentication-settings

feat: Authentication settings cannot be disabled if there will be no administrator user available to log in
Ryoji Shimizu 2 лет назад
Родитель
Сommit
3c0b34dd0c

+ 2 - 2
apps/app/src/components/Me/DisassociateModal.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback } from 'react';
 
+import type { IExternalAccountHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal,
@@ -9,13 +10,12 @@ import {
 } from 'reactstrap';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IExternalAccount } from '~/interfaces/external-account';
 import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 type Props = {
   isOpen: boolean,
   onClose: () => void,
-  accountForDisassociate: IExternalAccount,
+  accountForDisassociate: IExternalAccountHasId,
 }
 
 

+ 3 - 7
apps/app/src/features/questionnaire/interfaces/growi-info.ts

@@ -1,5 +1,7 @@
 import * as os from 'node:os';
 
+import { IExternalAuthProviderType } from '@growi/core';
+
 export const GrowiServiceType = {
   cloud: 'cloud',
   privateCloud: 'private-cloud',
@@ -23,13 +25,7 @@ export const GrowiDeploymentType = {
   node: 'node',
   others: 'others',
 } as const;
-export const GrowiExternalAuthProviderType = {
-  ldap: 'ldap',
-  saml: 'saml',
-  oicd: 'oidc',
-  google: 'google',
-  github: 'github',
-} as const;
+export const GrowiExternalAuthProviderType = IExternalAuthProviderType;
 
 export type GrowiServiceType = typeof GrowiServiceType[keyof typeof GrowiServiceType]
 type GrowiWikiType = typeof GrowiWikiType[keyof typeof GrowiWikiType]

+ 0 - 11
apps/app/src/interfaces/external-account.ts

@@ -1,11 +0,0 @@
-import { Ref } from '@growi/core';
-
-import { IUser } from '~/interfaces/user';
-
-
-export type IExternalAccount<ID = string> = {
-  _id: ID,
-  providerType: string,
-  accountId: string,
-  user: Ref<IUser>,
-}

+ 2 - 1
apps/app/src/server/routes/apiv3/index.js

@@ -9,6 +9,7 @@ import * as registerFormValidator from '../../middlewares/register-form-validato
 import g2gTransfer from './g2g-transfer';
 import importRoute from './import';
 import pageListing from './page-listing';
+import securitySettings from './security-settings';
 import * as userActivation from './user-activation';
 
 const logger = loggerFactory('growi:routes:apiv3'); // eslint-disable-line no-unused-vars
@@ -38,7 +39,7 @@ module.exports = (crowi, app) => {
   routerForAdmin.use('/export', require('./export')(crowi));
   routerForAdmin.use('/import', importRoute(crowi));
   routerForAdmin.use('/search', require('./search')(crowi));
-  routerForAdmin.use('/security-setting', require('./security-setting')(crowi));
+  routerForAdmin.use('/security-setting', securitySettings(crowi));
   routerForAdmin.use('/mongo', require('./mongo')(crowi));
   routerForAdmin.use('/slack-integration-settings', require('./slack-integration-settings')(crowi));
   routerForAdmin.use('/slack-integration-legacy-settings', require('./slack-integration-legacy-settings')(crowi));

+ 65 - 0
apps/app/src/server/routes/apiv3/security-settings/checkSetupStrategiesHasAdmin.ts

@@ -0,0 +1,65 @@
+import type { IExternalAuthProviderType } from '@growi/core';
+import mongoose from 'mongoose';
+
+interface AggregateResult {
+  count: number;
+}
+
+const checkLocalStrategyHasAdmin = async(): Promise<boolean> => {
+  const User = mongoose.model('User') as any;
+
+  const localAdmins: AggregateResult[] = await User.aggregate([
+    {
+      $match: {
+        admin: true,
+        status: User.STATUS_ACTIVE,
+        password: { $exists: true },
+      },
+    },
+    { $count: 'count' },
+  ]).exec();
+
+  return localAdmins.length > 0 && localAdmins[0].count > 0;
+};
+
+const checkExternalStrategiesHasAdmin = async(setupExternalStrategies: IExternalAuthProviderType[]): Promise<boolean> => {
+  const User = mongoose.model('User') as any;
+
+  const externalAdmins: AggregateResult[] = await User.aggregate([
+    { $match: { admin: true, status: User.STATUS_ACTIVE } },
+    {
+      $lookup: {
+        from: 'externalaccounts',
+        localField: '_id',
+        foreignField: 'user',
+        as: 'externalAccounts',
+      },
+    },
+    {
+      $match: {
+        'externalAccounts.providerType': { $in: setupExternalStrategies },
+      },
+    },
+    { $count: 'count' },
+  ]).exec();
+
+  return externalAdmins.length > 0 && externalAdmins[0].count > 0;
+};
+
+export const checkSetupStrategiesHasAdmin = async(setupStrategies: (IExternalAuthProviderType | 'local')[]): Promise<boolean> => {
+  if (setupStrategies.includes('local')) {
+    const isLocalStrategyHasAdmin = await checkLocalStrategyHasAdmin();
+    if (isLocalStrategyHasAdmin) {
+      return true;
+    }
+  }
+
+  const setupExternalStrategies = setupStrategies.filter(strategy => strategy !== 'local') as IExternalAuthProviderType[];
+  if (setupExternalStrategies.length === 0) {
+    return false;
+  }
+
+  const isExternalStrategiesHasAdmin = await checkExternalStrategiesHasAdmin(setupExternalStrategies);
+
+  return isExternalStrategiesHasAdmin;
+};

+ 158 - 148
apps/app/src/server/routes/apiv3/security-setting.js → apps/app/src/server/routes/apiv3/security-settings/index.js

@@ -2,12 +2,13 @@ import { ErrorV3 } from '@growi/core';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { PageDeleteConfigValue } from '~/interfaces/page-delete-config';
+import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
-import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { validateDeleteConfigs, prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
-import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
-import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { checkSetupStrategiesHasAdmin } from './checkSetupStrategiesHasAdmin';
 
 
 const logger = loggerFactory('growi:routes:apiv3:security-setting');
@@ -309,14 +310,14 @@ const validator = {
  *            description: local account automatically linked the email matched
  */
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
-  const adminRequired = require('../../middlewares/admin-required')(crowi);
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+  const adminRequired = require('~/server/middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
   const activityEvent = crowi.event('activity');
 
   async function updateAndReloadStrategySettings(authId, params) {
-    const { configManager, passportService } = crowi;
+    const { passportService } = crowi;
 
     // update config without publishing S2sMessage
     await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
@@ -348,100 +349,100 @@ module.exports = (crowi) => {
     const securityParams = {
       generalSetting: {
         restrictGuestMode: crowi.aclService.getGuestModeValue(),
-        pageDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageDeletionAuthority'),
-        pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
-        pageRecursiveDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageRecursiveDeletionAuthority'),
-        pageRecursiveCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
-        hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
-        hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
-        wikiMode: await crowi.configManager.getConfig('crowi', 'security:wikiMode'),
-        sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
+        pageDeletionAuthority: await configManager.getConfig('crowi', 'security:pageDeletionAuthority'),
+        pageCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
+        pageRecursiveDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveDeletionAuthority'),
+        pageRecursiveCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
+        hideRestrictedByOwner: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
+        hideRestrictedByGroup: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
+        wikiMode: await configManager.getConfig('crowi', 'security:wikiMode'),
+        sessionMaxAge: await configManager.getConfig('crowi', 'security:sessionMaxAge'),
       },
       shareLinkSetting: {
-        disableLinkSharing: await crowi.configManager.getConfig('crowi', 'security:disableLinkSharing'),
+        disableLinkSharing: await configManager.getConfig('crowi', 'security:disableLinkSharing'),
       },
       localSetting: {
-        useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
-        registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
-        registrationWhitelist: await crowi.configManager.getConfig('crowi', 'security:registrationWhitelist'),
-        isPasswordResetEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled'),
-        isEmailAuthenticationEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled'),
+        useOnlyEnvVarsForSomeOptions: await configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
+        registrationMode: await configManager.getConfig('crowi', 'security:registrationMode'),
+        registrationWhitelist: await configManager.getConfig('crowi', 'security:registrationWhitelist'),
+        isPasswordResetEnabled: await configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled'),
+        isEmailAuthenticationEnabled: await configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled'),
       },
       generalAuth: {
-        isLocalEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isEnabled'),
-        isLdapEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:isEnabled'),
-        isSamlEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isEnabled'),
-        isOidcEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isEnabled'),
-        isGoogleEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-google:isEnabled'),
-        isGitHubEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-github:isEnabled'),
+        isLocalEnabled: await configManager.getConfig('crowi', 'security:passport-local:isEnabled'),
+        isLdapEnabled: await configManager.getConfig('crowi', 'security:passport-ldap:isEnabled'),
+        isSamlEnabled: await configManager.getConfig('crowi', 'security:passport-saml:isEnabled'),
+        isOidcEnabled: await configManager.getConfig('crowi', 'security:passport-oidc:isEnabled'),
+        isGoogleEnabled: await configManager.getConfig('crowi', 'security:passport-google:isEnabled'),
+        isGitHubEnabled: await configManager.getConfig('crowi', 'security:passport-github:isEnabled'),
       },
       ldapAuth: {
-        serverUrl: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:serverUrl'),
-        isUserBind: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:isUserBind'),
-        ldapBindDN: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:bindDN'),
-        ldapBindDNPassword: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:bindDNPassword'),
-        ldapSearchFilter: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:searchFilter'),
-        ldapAttrMapUsername: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapUsername'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser'),
-        ldapAttrMapMail: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapMail'),
-        ldapAttrMapName: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapName'),
-        ldapGroupSearchBase: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:groupSearchBase'),
-        ldapGroupSearchFilter: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:groupSearchFilter'),
-        ldapGroupDnProperty: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:groupDnProperty'),
+        serverUrl: await configManager.getConfig('crowi', 'security:passport-ldap:serverUrl'),
+        isUserBind: await configManager.getConfig('crowi', 'security:passport-ldap:isUserBind'),
+        ldapBindDN: await configManager.getConfig('crowi', 'security:passport-ldap:bindDN'),
+        ldapBindDNPassword: await configManager.getConfig('crowi', 'security:passport-ldap:bindDNPassword'),
+        ldapSearchFilter: await configManager.getConfig('crowi', 'security:passport-ldap:searchFilter'),
+        ldapAttrMapUsername: await configManager.getConfig('crowi', 'security:passport-ldap:attrMapUsername'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser'),
+        ldapAttrMapMail: await configManager.getConfig('crowi', 'security:passport-ldap:attrMapMail'),
+        ldapAttrMapName: await configManager.getConfig('crowi', 'security:passport-ldap:attrMapName'),
+        ldapGroupSearchBase: await configManager.getConfig('crowi', 'security:passport-ldap:groupSearchBase'),
+        ldapGroupSearchFilter: await configManager.getConfig('crowi', 'security:passport-ldap:groupSearchFilter'),
+        ldapGroupDnProperty: await configManager.getConfig('crowi', 'security:passport-ldap:groupDnProperty'),
       },
       samlAuth: {
         missingMandatoryConfigKeys: await crowi.passportService.getSamlMissingMandatoryConfigKeys(),
-        useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions'),
-        samlEntryPoint: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:entryPoint'),
-        samlEnvVarEntryPoint: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:entryPoint'),
-        samlIssuer: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:issuer'),
-        samlEnvVarIssuer: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:issuer'),
-        samlCert: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:cert'),
-        samlEnvVarCert: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:cert'),
-        samlAttrMapId: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapId'),
-        samlEnvVarAttrMapId: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapId'),
-        samlAttrMapUsername: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapUsername'),
-        samlEnvVarAttrMapUsername: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapUsername'),
-        samlAttrMapMail: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapMail'),
-        samlEnvVarAttrMapMail: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapMail'),
-        samlAttrMapFirstName: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapFirstName'),
-        samlEnvVarAttrMapFirstName: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapFirstName'),
-        samlAttrMapLastName: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapLastName'),
-        samlEnvVarAttrMapLastName: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapLastName'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isSameUsernameTreatedAsIdenticalUser'),
-        isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isSameEmailTreatedAsIdenticalUser'),
-        samlABLCRule: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:ABLCRule'),
-        samlEnvVarABLCRule: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:ABLCRule'),
+        useOnlyEnvVarsForSomeOptions: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions'),
+        samlEntryPoint: await configManager.getConfigFromDB('crowi', 'security:passport-saml:entryPoint'),
+        samlEnvVarEntryPoint: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:entryPoint'),
+        samlIssuer: await configManager.getConfigFromDB('crowi', 'security:passport-saml:issuer'),
+        samlEnvVarIssuer: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:issuer'),
+        samlCert: await configManager.getConfigFromDB('crowi', 'security:passport-saml:cert'),
+        samlEnvVarCert: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:cert'),
+        samlAttrMapId: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapId'),
+        samlEnvVarAttrMapId: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapId'),
+        samlAttrMapUsername: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapUsername'),
+        samlEnvVarAttrMapUsername: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapUsername'),
+        samlAttrMapMail: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapMail'),
+        samlEnvVarAttrMapMail: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapMail'),
+        samlAttrMapFirstName: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapFirstName'),
+        samlEnvVarAttrMapFirstName: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapFirstName'),
+        samlAttrMapLastName: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapLastName'),
+        samlEnvVarAttrMapLastName: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapLastName'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-saml:isSameUsernameTreatedAsIdenticalUser'),
+        isSameEmailTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-saml:isSameEmailTreatedAsIdenticalUser'),
+        samlABLCRule: await configManager.getConfigFromDB('crowi', 'security:passport-saml:ABLCRule'),
+        samlEnvVarABLCRule: await configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:ABLCRule'),
       },
       oidcAuth: {
-        oidcProviderName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
-        oidcIssuerHost: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
-        oidcAuthorizationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
-        oidcTokenEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
-        oidcRevocationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
-        oidcIntrospectionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
-        oidcUserInfoEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
-        oidcEndSessionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
-        oidcRegistrationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
-        oidcJWKSUri: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
-        oidcClientId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
-        oidcClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
-        oidcAttrMapId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),
-        oidcAttrMapUserName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapUserName'),
-        oidcAttrMapName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapName'),
-        oidcAttrMapEmail: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapMail'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser'),
-        isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameEmailTreatedAsIdenticalUser'),
+        oidcProviderName: await configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
+        oidcIssuerHost: await configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
+        oidcAuthorizationEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
+        oidcTokenEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
+        oidcRevocationEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
+        oidcIntrospectionEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
+        oidcUserInfoEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
+        oidcEndSessionEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
+        oidcRegistrationEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
+        oidcJWKSUri: await configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
+        oidcClientId: await configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
+        oidcClientSecret: await configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
+        oidcAttrMapId: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),
+        oidcAttrMapUserName: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapUserName'),
+        oidcAttrMapName: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapName'),
+        oidcAttrMapEmail: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapMail'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser'),
+        isSameEmailTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-oidc:isSameEmailTreatedAsIdenticalUser'),
       },
       googleOAuth: {
-        googleClientId: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientId'),
-        googleClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
-        isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-google:isSameEmailTreatedAsIdenticalUser'),
+        googleClientId: await configManager.getConfig('crowi', 'security:passport-google:clientId'),
+        googleClientSecret: await configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
+        isSameEmailTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-google:isSameEmailTreatedAsIdenticalUser'),
       },
       githubOAuth: {
-        githubClientId: await crowi.configManager.getConfig('crowi', 'security:passport-github:clientId'),
-        githubClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-github:clientSecret'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-github:isSameUsernameTreatedAsIdenticalUser'),
+        githubClientId: await configManager.getConfig('crowi', 'security:passport-github:clientId'),
+        githubClientSecret: await configManager.getConfig('crowi', 'security:passport-github:clientSecret'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-github:isSameUsernameTreatedAsIdenticalUser'),
       },
     };
     return res.apiv3({ securityParams });
@@ -489,13 +490,22 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3('Can not turn everything off'), 405);
     }
 
+    if (!isEnabled) {
+      const isSetupStrategiesHasAdmin = await checkSetupStrategiesHasAdmin(setupStrategies);
+
+      // Return an error when disabling an strategy when there are no setup strategies with admin-enabled login
+      if (!isSetupStrategiesHasAdmin) {
+        return res.apiv3Err(new ErrorV3('Must have admin enabled authentication method'), 405);
+      }
+    }
+
     const enableParams = { [`security:passport-${authId}:isEnabled`]: isEnabled };
 
     try {
       await updateAndReloadStrategySettings(authId, enableParams);
 
       const responseParams = {
-        [`security:passport-${authId}:isEnabled`]: await crowi.configManager.getConfig('crowi', `security:passport-${authId}:isEnabled`),
+        [`security:passport-${authId}:isEnabled`]: await configManager.getConfig('crowi', `security:passport-${authId}:isEnabled`),
       };
       switch (authId) {
         case 'local':
@@ -624,22 +634,22 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3('Delete config values are not correct.', 'delete_config_not_normalized'));
     }
 
-    const wikiMode = await crowi.configManager.getConfig('crowi', 'security:wikiMode');
+    const wikiMode = await configManager.getConfig('crowi', 'security:wikiMode');
     if (wikiMode === 'private' || wikiMode === 'public') {
       logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
       delete updateData['security:restrictGuestMode'];
     }
     try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', updateData);
+      await configManager.updateConfigsInTheSameNamespace('crowi', updateData);
       const securitySettingParams = {
-        sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
-        restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
-        pageDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageDeletionAuthority'),
-        pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
-        pageRecursiveDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageRecursiveDeletionAuthority'),
-        pageRecursiveCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
-        hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
-        hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
+        sessionMaxAge: await configManager.getConfig('crowi', 'security:sessionMaxAge'),
+        restrictGuestMode: await configManager.getConfig('crowi', 'security:restrictGuestMode'),
+        pageDeletionAuthority: await configManager.getConfig('crowi', 'security:pageDeletionAuthority'),
+        pageCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
+        pageRecursiveDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveDeletionAuthority'),
+        pageRecursiveCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
+        hideRestrictedByOwner: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
+        hideRestrictedByGroup: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
       };
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_SECURITY_SETTINGS_UPDATE };
@@ -680,9 +690,9 @@ module.exports = (crowi) => {
       'security:disableLinkSharing': req.body.disableLinkSharing,
     };
     try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', updateData);
+      await configManager.updateConfigsInTheSameNamespace('crowi', updateData);
       const securitySettingParams = {
-        disableLinkSharing: crowi.configManager.getConfig('crowi', 'security:disableLinkSharing'),
+        disableLinkSharing: configManager.getConfig('crowi', 'security:disableLinkSharing'),
       };
       // eslint-disable-next-line max-len
       const parameters = { action: updateData['security:disableLinkSharing'] ? SupportedAction.ACTION_ADMIN_REJECT_SHARE_LINK : SupportedAction.ACTION_ADMIN_PERMIT_SHARE_LINK };
@@ -799,10 +809,10 @@ module.exports = (crowi) => {
       await updateAndReloadStrategySettings('local', requestParams);
 
       const localSettingParams = {
-        registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
-        registrationWhitelist: await crowi.configManager.getConfig('crowi', 'security:registrationWhitelist'),
-        isPasswordResetEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled'),
-        isEmailAuthenticationEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled'),
+        registrationMode: await configManager.getConfig('crowi', 'security:registrationMode'),
+        registrationWhitelist: await configManager.getConfig('crowi', 'security:registrationWhitelist'),
+        isPasswordResetEnabled: await configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled'),
+        isEmailAuthenticationEnabled: await configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_AUTH_ID_PASS_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
@@ -856,18 +866,18 @@ module.exports = (crowi) => {
       await updateAndReloadStrategySettings('ldap', requestParams);
 
       const securitySettingParams = {
-        serverUrl: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:serverUrl'),
-        isUserBind: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:isUserBind'),
-        ldapBindDN: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:bindDN'),
-        ldapBindDNPassword: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:bindDNPassword'),
-        ldapSearchFilter: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:searchFilter'),
-        ldapAttrMapUsername: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapUsername'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser'),
-        ldapAttrMapMail: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapMail'),
-        ldapAttrMapName: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapName'),
-        ldapGroupSearchBase: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:groupSearchBase'),
-        ldapGroupSearchFilter: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:groupSearchFilter'),
-        ldapGroupDnProperty: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:groupDnProperty'),
+        serverUrl: await configManager.getConfig('crowi', 'security:passport-ldap:serverUrl'),
+        isUserBind: await configManager.getConfig('crowi', 'security:passport-ldap:isUserBind'),
+        ldapBindDN: await configManager.getConfig('crowi', 'security:passport-ldap:bindDN'),
+        ldapBindDNPassword: await configManager.getConfig('crowi', 'security:passport-ldap:bindDNPassword'),
+        ldapSearchFilter: await configManager.getConfig('crowi', 'security:passport-ldap:searchFilter'),
+        ldapAttrMapUsername: await configManager.getConfig('crowi', 'security:passport-ldap:attrMapUsername'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser'),
+        ldapAttrMapMail: await configManager.getConfig('crowi', 'security:passport-ldap:attrMapMail'),
+        ldapAttrMapName: await configManager.getConfig('crowi', 'security:passport-ldap:attrMapName'),
+        ldapGroupSearchBase: await configManager.getConfig('crowi', 'security:passport-ldap:groupSearchBase'),
+        ldapGroupSearchFilter: await configManager.getConfig('crowi', 'security:passport-ldap:groupSearchFilter'),
+        ldapGroupDnProperty: await configManager.getConfig('crowi', 'security:passport-ldap:groupDnProperty'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_AUTH_LDAP_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
@@ -910,7 +920,7 @@ module.exports = (crowi) => {
     for (const configKey of crowi.passportService.mandatoryConfigKeysForSaml) {
       const key = configKey.replace('security:passport-saml:', '');
       const formValue = req.body[key];
-      if (crowi.configManager.getConfigFromEnvVars('crowi', configKey) === null && formValue == null) {
+      if (configManager.getConfigFromEnvVars('crowi', configKey) === null && formValue == null) {
         const formItemName = req.t(`security_setting.form_item_name.${key}`);
         invalidValues.push(req.t('form_validation.required', formItemName));
       }
@@ -950,17 +960,17 @@ module.exports = (crowi) => {
 
       const securitySettingParams = {
         missingMandatoryConfigKeys: await crowi.passportService.getSamlMissingMandatoryConfigKeys(),
-        samlEntryPoint: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:entryPoint'),
-        samlIssuer: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:issuer'),
-        samlCert: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:cert'),
-        samlAttrMapId: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapId'),
-        samlAttrMapUsername: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapUsername'),
-        samlAttrMapMail: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapMail'),
-        samlAttrMapFirstName: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapFirstName'),
-        samlAttrMapLastName: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapLastName'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isSameUsernameTreatedAsIdenticalUser'),
-        isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isSameEmailTreatedAsIdenticalUser'),
-        samlABLCRule: await crowi.configManager.getConfig('crowi', 'security:passport-saml:ABLCRule'),
+        samlEntryPoint: await configManager.getConfigFromDB('crowi', 'security:passport-saml:entryPoint'),
+        samlIssuer: await configManager.getConfigFromDB('crowi', 'security:passport-saml:issuer'),
+        samlCert: await configManager.getConfigFromDB('crowi', 'security:passport-saml:cert'),
+        samlAttrMapId: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapId'),
+        samlAttrMapUsername: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapUsername'),
+        samlAttrMapMail: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapMail'),
+        samlAttrMapFirstName: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapFirstName'),
+        samlAttrMapLastName: await configManager.getConfigFromDB('crowi', 'security:passport-saml:attrMapLastName'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-saml:isSameUsernameTreatedAsIdenticalUser'),
+        isSameEmailTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-saml:isSameEmailTreatedAsIdenticalUser'),
+        samlABLCRule: await configManager.getConfig('crowi', 'security:passport-saml:ABLCRule'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_AUTH_SAML_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
@@ -1020,24 +1030,24 @@ module.exports = (crowi) => {
       await updateAndReloadStrategySettings('oidc', requestParams);
 
       const securitySettingParams = {
-        oidcProviderName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
-        oidcIssuerHost: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
-        oidcAuthorizationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
-        oidcTokenEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
-        oidcRevocationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
-        oidcIntrospectionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
-        oidcUserInfoEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
-        oidcEndSessionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
-        oidcRegistrationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
-        oidcJWKSUri: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
-        oidcClientId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
-        oidcClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
-        oidcAttrMapId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),
-        oidcAttrMapUserName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapUserName'),
-        oidcAttrMapName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapName'),
-        oidcAttrMapEmail: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapMail'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser'),
-        isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameEmailTreatedAsIdenticalUser'),
+        oidcProviderName: await configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
+        oidcIssuerHost: await configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
+        oidcAuthorizationEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
+        oidcTokenEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
+        oidcRevocationEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
+        oidcIntrospectionEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
+        oidcUserInfoEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
+        oidcEndSessionEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
+        oidcRegistrationEndpoint: await configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
+        oidcJWKSUri: await configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
+        oidcClientId: await configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
+        oidcClientSecret: await configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
+        oidcAttrMapId: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),
+        oidcAttrMapUserName: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapUserName'),
+        oidcAttrMapName: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapName'),
+        oidcAttrMapEmail: await configManager.getConfig('crowi', 'security:passport-oidc:attrMapMail'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser'),
+        isSameEmailTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-oidc:isSameEmailTreatedAsIdenticalUser'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_AUTH_OIDC_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
@@ -1083,9 +1093,9 @@ module.exports = (crowi) => {
       await updateAndReloadStrategySettings('google', requestParams);
 
       const securitySettingParams = {
-        googleClientId: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientId'),
-        googleClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
-        isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-google:isSameEmailTreatedAsIdenticalUser'),
+        googleClientId: await configManager.getConfig('crowi', 'security:passport-google:clientId'),
+        googleClientSecret: await configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
+        isSameEmailTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-google:isSameEmailTreatedAsIdenticalUser'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_AUTH_GOOGLE_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
@@ -1130,9 +1140,9 @@ module.exports = (crowi) => {
       await updateAndReloadStrategySettings('github', requestParams);
 
       const securitySettingParams = {
-        githubClientId: await crowi.configManager.getConfig('crowi', 'security:passport-github:clientId'),
-        githubClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-github:clientSecret'),
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-github:isSameUsernameTreatedAsIdenticalUser'),
+        githubClientId: await configManager.getConfig('crowi', 'security:passport-github:clientId'),
+        githubClientSecret: await configManager.getConfig('crowi', 'security:passport-github:clientSecret'),
+        isSameUsernameTreatedAsIdenticalUser: await configManager.getConfig('crowi', 'security:passport-github:isSameUsernameTreatedAsIdenticalUser'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_AUTH_GITHUB_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);

+ 3 - 4
apps/app/src/stores/personal-settings.tsx

@@ -1,8 +1,7 @@
-import { ErrorV3 } from '@growi/core';
+import type { IExternalAccountHasId, IExternalAuthProviderType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
-import { IExternalAccount } from '~/interfaces/external-account';
 import { IUser } from '~/interfaces/user';
 import { useIsGuestUser } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
@@ -31,7 +30,7 @@ export type IPersonalSettingsInfoOption = {
   sync: () => void,
   updateBasicInfo: () => Promise<void>,
   associateLdapAccount: (account: { username: string, password: string }) => Promise<void>,
-  disassociateLdapAccount: (account: { providerType: string, accountId: string }) => Promise<void>,
+  disassociateLdapAccount: (account: { providerType: IExternalAuthProviderType, accountId: string }) => Promise<void>,
 }
 
 export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
@@ -104,7 +103,7 @@ export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUse
   };
 };
 
-export const useSWRxPersonalExternalAccounts = (): SWRResponse<IExternalAccount[], Error> => {
+export const useSWRxPersonalExternalAccounts = (): SWRResponse<IExternalAccountHasId[], Error> => {
   return useSWR(
     '/personal-setting/external-accounts',
     endpoint => apiv3Get(endpoint).then(response => response.data.externalAccounts),

+ 21 - 0
packages/core/src/interfaces/external-account.ts

@@ -0,0 +1,21 @@
+import type { Ref } from './common';
+import type { HasObjectId } from './has-object-id';
+import type { IUser } from './user';
+
+export const IExternalAuthProviderType = {
+  ldap: 'ldap',
+  saml: 'saml',
+  oicd: 'oidc',
+  google: 'google',
+  github: 'github',
+} as const;
+
+export type IExternalAuthProviderType = typeof IExternalAuthProviderType[keyof typeof IExternalAuthProviderType]
+
+export type IExternalAccount = {
+  providerType: IExternalAuthProviderType,
+  accountId: string,
+  user: Ref<IUser>,
+}
+
+export type IExternalAccountHasId = IExternalAccount & HasObjectId

+ 1 - 0
packages/core/src/interfaces/index.ts

@@ -1,6 +1,7 @@
 export * from './attachment';
 export * from './color-scheme';
 export * from './common';
+export * from './external-account';
 export * from './growi-facade';
 export * from './growi-theme-metadata';
 export * from './has-object-id';

+ 2 - 1
packages/core/src/interfaces/user.ts

@@ -1,5 +1,6 @@
 import type { IAttachment } from './attachment';
 import type { Ref } from './common';
+import type { IExternalAuthProviderType } from './external-account';
 import type { HasObjectId } from './has-object-id';
 import type { Lang } from './lang';
 
@@ -54,7 +55,7 @@ export type IUserGroupRelationHasId = IUserGroupRelation & HasObjectId;
 
 export type IAdminExternalAccount = {
   _id: string,
-  providerType: string,
+  providerType: IExternalAuthProviderType,
   accountId: string,
   user: IUser,
   createdAt: Date,