Bläddra i källkod

get auth provider type from configs

Futa Arai 2 år sedan
förälder
incheckning
23f48c76b9

+ 11 - 9
apps/app/public/static/locales/en_US/admin.json

@@ -1060,6 +1060,7 @@
     "description_form_detail": "Please note that edited value will be overwritten on next sync if description mapper is set in sync settings",
     "only_description_edit_allowed": "Only description can be edited for external user groups",
     "sync_succeeded": "Sync succeeded",
+    "sync_failed": "Sync failed",
     "ldap": {
       "group_sync_settings": "LDAP Group Sync Settings",
       "group_search_base_DN": "Group Search Base DN",
@@ -1083,16 +1084,17 @@
       "group_sync_settings": "Keycloak Group Sync Settings",
       "host": "Host",
       "host_detail": "Keycloak host URL",
-      "realm": "Realm",
-      "realm_detail": "Realm that contains the groups to sync",
-      "group_sync_client_name": "Group Sync Client Name",
-      "group_sync_client_name_detail": "Name of the client in the master realm to use for group sync",
-      "group_sync_client_id": "Group Sync Client ID",
-      "group_sync_client_id_detail": "ID of the client in the master realm to use for group sync",
-      "group_sync_client_secret": "Group Sync Client Secret",
-      "group_sync_client_secret_detail": "Secret of the client in the master realm to use for group sync",
+      "group_realm": "Group Realm",
+      "group_realm_detail": "Realm that contains the groups to sync",
+      "group_sync_client_realm": "Realm of client used to request to Admin API",
+      "group_sync_client_realm_detail": "Realm that contains the client used to authenticate to request to Keycloak admin API",
+      "group_sync_client_id": "Client ID",
+      "group_sync_client_id_detail": "Id of the client used to authenticate to request to Keycloak admin API",
+      "group_sync_client_secret": "Client Secret",
+      "group_sync_client_secret_detail": "Id of the secret used to authenticate to request to Keycloak admin API",
       "updated_group_sync_settings": "Updated Keycloak group sync settings",
-      "preserve_deleted_keycloak_groups": "Preserve Deleted Keycloak Groups"
+      "preserve_deleted_keycloak_groups": "Preserve Deleted Keycloak Groups",
+      "auth_not_set": "Please set up and enable OIDC or SAML with Keycloak in security settings before sync"
     },
     "auto_generate_user_on_sync": "Auto Generate User on Sync",
     "description_mapper_detail": "Attribute to map as group description. Description can be edited after sync. However, when a mapper is set, the edited value can possibly be overwritten by the next sync."

+ 10 - 9
apps/app/public/static/locales/ja_JP/admin.json

@@ -1091,16 +1091,17 @@
       "group_sync_settings": "Keycloak グループ同期設定",
       "host": "Host",
       "host_detail": "Keycloak ホスト URL",
-      "realm": "Realm",
-      "realm_detail": "同期対象のグループがある realm",
-      "group_sync_client_name": "グループ同期に使う Client 名",
-      "group_sync_client_name_detail": "Master realm にある、グループ同期に使う Client 名",
-      "group_sync_client_id": "グループ同期に使う Client の ID",
-      "group_sync_client_id_detail": "Master realm にある、グループ同期に使う Client ID",
-      "group_sync_client_secret": "グループ同期に使う Client の Secret",
-      "group_sync_client_secret_detail": "Master realm にある、グループ同期に使う Client の secret",
+      "group_realm": "Group Realm",
+      "group_realm_detail": "同期対象のグループがある realm",
+      "group_sync_client_realm": "Admin API を叩くための Client がある Realm",
+      "group_sync_client_realm_detail": "Keycloak admin API を叩くための認証に使う client がある realm",
+      "group_sync_client_id": "Client の ID",
+      "group_sync_client_id_detail": "Keycloak admin API を叩くための認証に使う client の Client ID",
+      "group_sync_client_secret": "Client の Secret",
+      "group_sync_client_secret_detail": "Keycloak admin API を叩くための認証に使う client の secret",
       "updated_group_sync_settings": "Keycloak グループ同期設定を更新しました",
-      "preserve_deleted_keycloak_groups": "Keycloak から削除されたグループを GROWI に残す"
+      "preserve_deleted_keycloak_groups": "Keycloak から削除されたグループを GROWI に残す",
+      "auth_not_set": "同期実行前に、セキュリティ設定で Keycloak を使った OIDC または SAML 認証を設定し、有効にしてください"
     },
     "auto_generate_user_on_sync": "作成されていない GROWI アカウントを自動生成する",
     "description_mapper_detail": "グループの「説明」として読み込む属性。「説明」は同期後に編集可能です。ただし、mapper が設定されている場合、編集内容は再同期によって上書きされます。"

+ 10 - 9
apps/app/public/static/locales/zh_CN/admin.json

@@ -1091,16 +1091,17 @@
       "group_sync_settings": "Keycloak Group Sync Settings",
       "host": "Host",
       "host_detail": "Keycloak host URL",
-      "realm": "Realm",
-      "realm_detail": "Realm that contains the groups to sync",
-      "group_sync_client_name": "Group Sync Client Name",
-      "group_sync_client_name_detail": "Name of the client in the master realm to use for group sync",
-      "group_sync_client_id": "Group Sync Client ID",
-      "group_sync_client_id_detail": "ID of the client in the master realm to use for group sync",
-      "group_sync_client_secret": "Group Sync Client Secret",
-      "group_sync_client_secret_detail": "Secret of the client in the master realm to use for group sync",
+      "group_realm": "Group Realm",
+      "group_realm_detail": "Realm that contains the groups to sync",
+      "group_sync_client_realm": "Realm of client used to request to Admin API",
+      "group_sync_client_realm_detail": "Realm that contains the client used to authenticate to request to Keycloak admin API",
+      "group_sync_client_id": "Client ID",
+      "group_sync_client_id_detail": "Id of the client used to authenticate to request to Keycloak admin API",
+      "group_sync_client_secret": "Client Secret",
+      "group_sync_client_secret_detail": "Id of the secret used to authenticate to request to Keycloak admin API",
       "updated_group_sync_settings": "Updated Keycloak group sync settings",
-      "preserve_deleted_keycloak_groups": "Preserve Deleted Keycloak Groups"
+      "preserve_deleted_keycloak_groups": "Preserve Deleted Keycloak Groups",
+      "auth_not_set": "Please set up and enable OIDC or SAML with Keycloak in security settings before sync"
     },
     "auto_generate_user_on_sync": "Auto Generate User on Sync",
     "description_mapper_detail": "Attribute to map as group description. Description can be edited after sync. However, when a mapper is set, the edited value can possibly be overwritten by the next sync."

+ 16 - 16
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/KeycloakGroupSyncSettingsForm.tsx

@@ -16,8 +16,8 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
 
   const [formValues, setFormValues] = useState<KeycloakGroupSyncSettings>({
     keycloakHost: '',
-    keycloakRealm: '',
-    keycloakGroupSyncClientName: '',
+    keycloakGroupRealm: '',
+    keycloakGroupSyncClientRealm: '',
     keycloakGroupSyncClientID: '',
     keycloakGroupSyncClientSecret: '',
     autoGenerateUserOnKeycloakGroupSync: false,
@@ -68,43 +68,43 @@ export const KeycloakGroupSyncSettingsForm: FC = () => {
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="keycloakRealm" className="text-left text-md-right col-md-3 col-form-label">
-            {t('external_user_group.keycloak.realm')}
+          <label htmlFor="keycloakGroupRealm" className="text-left text-md-right col-md-3 col-form-label">
+            {t('external_user_group.keycloak.group_realm')}
           </label>
           <div className="col-md-6">
             <input
               className="form-control"
               required
               type="text"
-              name="keycloakRealm"
-              id="keycloakRealm"
-              value={formValues.keycloakRealm}
-              onChange={e => setFormValues({ ...formValues, keycloakRealm: e.target.value })}
+              name="keycloakGroupRealm"
+              id="keycloakGroupRealm"
+              value={formValues.keycloakGroupRealm}
+              onChange={e => setFormValues({ ...formValues, keycloakGroupRealm: e.target.value })}
             />
             <p className="form-text text-muted">
               <small>
-                {t('external_user_group.keycloak.realm_detail')} <br />
+                {t('external_user_group.keycloak.group_realm_detail')} <br />
               </small>
             </p>
           </div>
         </div>
         <div className="row form-group">
-          <label htmlFor="keycloakGroupSyncClientName" className="text-left text-md-right col-md-3 col-form-label">
-            {t('external_user_group.keycloak.group_sync_client_name')}
+          <label htmlFor="keycloakGroupSyncClientRealm" className="text-left text-md-right col-md-3 col-form-label">
+            {t('external_user_group.keycloak.group_sync_client_realm')}
           </label>
           <div className="col-md-6">
             <input
               className="form-control"
               required
               type="text"
-              name="keycloakGroupSyncClientName"
-              id="keycloakGroupSyncClientName"
-              value={formValues.keycloakGroupSyncClientName}
-              onChange={e => setFormValues({ ...formValues, keycloakGroupSyncClientName: e.target.value })}
+              name="keycloakGroupSyncClientRealm"
+              id="keycloakGroupSyncClientRealm"
+              value={formValues.keycloakGroupSyncClientRealm}
+              onChange={e => setFormValues({ ...formValues, keycloakGroupSyncClientRealm: e.target.value })}
             />
             <p className="form-text text-muted">
               <small>
-                {t('external_user_group.keycloak.group_sync_client_name_detail')} <br />
+                {t('external_user_group.keycloak.group_sync_client_realm_detail')} <br />
               </small>
             </p>
           </div>

+ 2 - 2
apps/app/src/features/external-user-group/interfaces/external-user-group.ts

@@ -36,8 +36,8 @@ export interface LdapGroupSyncSettings {
 
 export interface KeycloakGroupSyncSettings {
   keycloakHost: string
-  keycloakRealm: string
-  keycloakGroupSyncClientName: string
+  keycloakGroupRealm: string
+  keycloakGroupSyncClientRealm: string
   keycloakGroupSyncClientID: string
   keycloakGroupSyncClientSecret: string
   autoGenerateUserOnKeycloakGroupSync: boolean

+ 32 - 9
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -48,8 +48,8 @@ module.exports = (crowi: Crowi): Router => {
     ],
     keycloakSyncSettings: [
       body('keycloakHost').exists({ checkFalsy: true }).isString(),
-      body('keycloakRealm').exists({ checkFalsy: true }).isString(),
-      body('keycloakGroupSyncClientName').exists({ checkFalsy: true }).isString(),
+      body('keycloakGroupRealm').exists({ checkFalsy: true }).isString(),
+      body('keycloakGroupSyncClientRealm').exists({ checkFalsy: true }).isString(),
       body('keycloakGroupSyncClientID').exists({ checkFalsy: true }).isString(),
       body('keycloakGroupSyncClientSecret').exists({ checkFalsy: true }).isString(),
       body('autoGenerateUserOnKeycloakGroupSync').exists().isBoolean(),
@@ -226,8 +226,8 @@ module.exports = (crowi: Crowi): Router => {
   router.get('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
     const settings = {
       keycloakHost: configManager?.getConfig('crowi', 'external-user-group:keycloak:host'),
-      keycloakRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:realm'),
-      keycloakGroupSyncClientName: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientName'),
+      keycloakGroupRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm'),
+      keycloakGroupSyncClientRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm'),
       keycloakGroupSyncClientID: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID'),
       keycloakGroupSyncClientSecret: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret'),
       autoGenerateUserOnKeycloakGroupSync: configManager?.getConfig('crowi', 'external-user-group:keycloak:autoGenerateUserOnGroupSync'),
@@ -279,8 +279,8 @@ module.exports = (crowi: Crowi): Router => {
 
       const params = {
         'external-user-group:keycloak:host': req.body.keycloakHost,
-        'external-user-group:keycloak:realm': req.body.keycloakRealm,
-        'external-user-group:keycloak:groupSyncClientName': req.body.keycloakGroupSyncClientName,
+        'external-user-group:keycloak:groupRealm': req.body.keycloakGroupRealm,
+        'external-user-group:keycloak:groupSyncClientRealm': req.body.keycloakGroupSyncClientRealm,
         'external-user-group:keycloak:groupSyncClientID': req.body.keycloakGroupSyncClientID,
         'external-user-group:keycloak:groupSyncClientSecret': req.body.keycloakGroupSyncClientSecret,
         'external-user-group:keycloak:autoGenerateUserOnGroupSync': req.body.autoGenerateUserOnKeycloakGroupSync,
@@ -312,14 +312,37 @@ module.exports = (crowi: Crowi): Router => {
   });
 
   router.put('/keycloak/sync', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    const getAuthProviderType = () => {
+      const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
+      const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
+
+      // starts with kcHost, contains kcGroupRealm in path
+      // see: https://regex101.com/r/3ihDmf/1
+      const regex = new RegExp(`^${kcHost}/.*/${kcGroupRealm}(/|$).*`);
+
+      const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
+      const oidcIssuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost');
+
+      if (isOidcEnabled && regex.test(oidcIssuerHost)) return 'oidc';
+
+      const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
+      const samlEntryPoint = configManager.getConfig('crowi', 'security:passport-saml:entryPoint');
+
+      if (isSamlEnabled && regex.test(samlEntryPoint)) return 'saml';
+
+      return null;
+    };
+
+    const authProviderType = getAuthProviderType();
+    if (authProviderType == null) return res.apiv3Err('external_user_group.keycloak.auth_not_set', 500);
+
     try {
-      const keycloakUserGroupSyncService = new KeycloakUserGroupSyncService();
-      await keycloakUserGroupSyncService.auth();
+      const keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(authProviderType);
       await keycloakUserGroupSyncService.syncExternalUserGroups();
     }
     catch (err) {
       logger.error(err);
-      return res.apiv3Err(err.message, 500);
+      return res.apiv3Err('external_user_group.sync_failed', 500);
     }
 
     return res.apiv3({}, 204);

+ 38 - 28
apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.ts

@@ -16,38 +16,25 @@ class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
 
   kcAdminClient: KeycloakAdminClient;
 
-  realm: string;
-
-  groupDescriptionAttribute: string;
-
-  constructor() {
-    const keycloakHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
-    const keycloakRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:realm');
-    const keycloakGroupDescriptionAttribute = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute');
-    // TODO: allow user to choose 'oidc' or 'saml' for keycloak in settings
-    super(ExternalGroupProviderType.ldap, 'oidc');
-    this.kcAdminClient = new KeycloakAdminClient({ baseUrl: keycloakHost });
-    this.realm = keycloakRealm;
-    this.groupDescriptionAttribute = keycloakGroupDescriptionAttribute;
-  }
+  realm: string; // realm that contains the groups
 
-  async auth(): Promise<void> {
-    const keycloakGroupSyncClientName = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientName');
-    const keycloakGroupSyncClientID: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID');
-    const keycloakGroupSyncClientSecret: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret');
+  groupDescriptionAttribute: string; // attribute to map to group description
 
-    await this.kcAdminClient.auth({
-      // grantType: 'client_credentials',
-      // clientId: keycloakGroupSyncClientID,
-      // clientSecret: keycloakGroupSyncClientSecret,
-      grantType: 'password',
-      username: 'admin',
-      password: 'admin',
-      clientId: keycloakGroupSyncClientID,
-    });
+  constructor(authProviderType: string) {
+    const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
+    const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
+    const kcGroupSyncClientRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm');
+    const kcGroupDescriptionAttribute = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute');
+
+    super(ExternalGroupProviderType.keycloak, authProviderType);
+    this.kcAdminClient = new KeycloakAdminClient({ baseUrl: kcHost, realmName: kcGroupSyncClientRealm });
+    this.realm = kcGroupRealm;
+    this.groupDescriptionAttribute = kcGroupDescriptionAttribute;
   }
 
   async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
+    await this.auth();
+
     // Type is 'GroupRepresentation', but 'find' does not return 'attributes' field. Hence, attribute for description is not present.
     const rootGroups = await this.kcAdminClient.groups.find({ realm: this.realm });
 
@@ -55,6 +42,23 @@ class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
       .filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
   }
 
+  /**
+   * Authenticate to group sync client using client credentials grant type
+   */
+  private async auth(): Promise<void> {
+    const kcGroupSyncClientID: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID');
+    const kcGroupSyncClientSecret: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret');
+
+    await this.kcAdminClient.auth({
+      grantType: 'client_credentials',
+      clientId: kcGroupSyncClientID,
+      clientSecret: kcGroupSyncClientSecret,
+    });
+  }
+
+  /**
+   * Convert GroupRepresentation response returned from Keycloak to ExternalUserGroupTreeNode
+   */
   private async groupRepresentationToTreeNode(group: GroupRepresentation): Promise<ExternalUserGroupTreeNode | null> {
     if (group.id == null || group.name == null) return null;
 
@@ -84,15 +88,21 @@ class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
     };
   }
 
+  /**
+   * Fetch group detail from Keycloak and return group description
+   */
   private async getGroupDescription(groupId: string): Promise<string | null> {
     if (this.groupDescriptionAttribute == null) return null;
 
     const groupDetail = await this.kcAdminClient.groups.findOne({ id: groupId, realm: this.realm });
 
-    const description = groupDetail?.attributes?.[this.groupDescriptionAttribute];
+    const description = groupDetail?.attributes?.[this.groupDescriptionAttribute]?.[0];
     return typeof description === 'string' ? description : null;
   }
 
+  /**
+   * Convert UserRepresentation array response returned from Keycloak to ExternalUserInfo
+   */
   private userRepresentationsToExternalUserInfos(userRepresentations: UserRepresentation[]): ExternalUserInfo[] {
     const externalUserGroupsWithNull: (ExternalUserInfo | null)[] = userRepresentations.map((userRepresentation) => {
       if (userRepresentation.id != null && userRepresentation.username != null) {