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

Merge branch 'master' into support/apply-bootstrap4

# Conflicts:
#	src/client/styles/scss/_handsontable.scss
#	src/client/styles/scss/_page.scss
#	src/client/styles/scss/style-app.scss
#	src/server/views/admin/security.html
yusuketk 6 лет назад
Родитель
Сommit
98962e988d
92 измененных файлов с 6580 добавлено и 3228 удалено
  1. 5 2
      CHANGES.md
  2. 4 2
      package.json
  3. 8 0
      resource/cdn-manifests.js
  4. 7 0
      resource/locales/en-US/sandbox-diagrams.md
  5. 71 0
      resource/locales/en-US/sandbox-math.md
  6. 22 358
      resource/locales/en-US/sandbox.md
  7. 55 36
      resource/locales/en-US/translation.json
  8. 7 0
      resource/locales/ja/sandbox-diagrams.md
  9. 71 0
      resource/locales/ja/sandbox-math.md
  10. 22 357
      resource/locales/ja/sandbox.md
  11. 61 36
      resource/locales/ja/translation.json
  12. 35 0
      src/client/js/admin.jsx
  13. 1 1
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  14. 1 1
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  15. 147 0
      src/client/js/components/Admin/Security/BasicSecuritySetting.jsx
  16. 41 0
      src/client/js/components/Admin/Security/FacebookSecuritySetting.jsx
  17. 218 0
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  18. 225 0
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  19. 171 0
      src/client/js/components/Admin/Security/LdapAuthTestModal.jsx
  20. 432 0
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  21. 202 0
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  22. 352 0
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  23. 550 0
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  24. 124 0
      src/client/js/components/Admin/Security/SecurityManagement.jsx
  25. 231 0
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  26. 226 0
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  27. 79 0
      src/client/js/components/Drawio.jsx
  28. 48 0
      src/client/js/components/Page.jsx
  29. 19 0
      src/client/js/components/PageEditor/CodeMirrorEditor.jsx
  30. 159 0
      src/client/js/components/PageEditor/DrawioModal.jsx
  31. 3 0
      src/client/js/components/PageEditor/EditorIcon.jsx
  32. 161 0
      src/client/js/components/PageEditor/MarkdownDrawioUtil.js
  33. 74 0
      src/client/js/services/AdminBasicSecurityContainer.js
  34. 215 0
      src/client/js/services/AdminGeneralSecurityContainer.js
  35. 99 0
      src/client/js/services/AdminGitHubSecurityContainer.js
  36. 102 0
      src/client/js/services/AdminGoogleSecurityContainer.js
  37. 203 0
      src/client/js/services/AdminLdapSecurityContainer.js
  38. 85 0
      src/client/js/services/AdminLocalSecurityContainer.js
  39. 183 0
      src/client/js/services/AdminOidcSecurityContainer.js
  40. 196 0
      src/client/js/services/AdminSamlSecurityContainer.js
  41. 99 0
      src/client/js/services/AdminTwitterSecurityContainer.js
  42. 15 0
      src/client/js/services/AppContainer.js
  43. 2 0
      src/client/js/util/GrowiRenderer.js
  44. 148 0
      src/client/js/util/interceptor/drawio-interceptor.js
  45. 9 0
      src/client/js/util/markdown-it/drawio-viewer.js
  46. 3 0
      src/client/styles/scss/_drawio.scss
  47. 4 18
      src/client/styles/scss/_handsontable.scss
  48. 27 0
      src/client/styles/scss/_mixins.scss
  49. 23 0
      src/client/styles/scss/_page.scss
  50. 2 0
      src/client/styles/scss/style-app.scss
  51. 12 0
      src/lib/util/removeNullPropertyFromObject.js
  52. 12 12
      src/server/crowi/index.js
  53. 0 7
      src/server/form/admin/custombehavior.js
  54. 0 7
      src/server/form/admin/customcss.js
  55. 0 12
      src/server/form/admin/customfeatures.js
  56. 0 7
      src/server/form/admin/customheader.js
  57. 0 8
      src/server/form/admin/customhighlightJsStyle.js
  58. 0 8
      src/server/form/admin/customlayout.js
  59. 0 7
      src/server/form/admin/customscript.js
  60. 0 7
      src/server/form/admin/customtheme.js
  61. 0 7
      src/server/form/admin/customtitle.js
  62. 0 10
      src/server/form/admin/securityGeneral.js
  63. 0 8
      src/server/form/admin/securityPassportBasic.js
  64. 0 10
      src/server/form/admin/securityPassportGitHub.js
  65. 0 10
      src/server/form/admin/securityPassportGoogle.js
  66. 0 23
      src/server/form/admin/securityPassportLdap.js
  67. 0 11
      src/server/form/admin/securityPassportLocal.js
  68. 0 17
      src/server/form/admin/securityPassportOidc.js
  69. 0 18
      src/server/form/admin/securityPassportSaml.js
  70. 0 10
      src/server/form/admin/securityPassportTwitter.js
  71. 0 9
      src/server/form/index.js
  72. 15 9
      src/server/models/config.js
  73. 13 256
      src/server/routes/admin.js
  74. 2 0
      src/server/routes/apiv3/index.js
  75. 985 0
      src/server/routes/apiv3/security-setting.js
  76. 0 9
      src/server/routes/index.js
  77. 12 12
      src/server/routes/installer.js
  78. 11 10
      src/server/routes/login-passport.js
  79. 154 87
      src/server/service/passport.js
  80. 1 255
      src/server/views/admin/security.html
  81. 0 73
      src/server/views/admin/widget/passport/basic.html
  82. 0 6
      src/server/views/admin/widget/passport/facebook.html
  83. 0 120
      src/server/views/admin/widget/passport/github.html
  84. 0 120
      src/server/views/admin/widget/passport/google-oauth.html
  85. 0 363
      src/server/views/admin/widget/passport/ldap.html
  86. 0 84
      src/server/views/admin/widget/passport/local.html
  87. 0 218
      src/server/views/admin/widget/passport/oidc.html
  88. 0 456
      src/server/views/admin/widget/passport/saml.html
  89. 0 120
      src/server/views/admin/widget/passport/twitter.html
  90. 1 1
      src/server/views/widget/alert_siteurl_undefined.html
  91. 76 0
      src/test/service/passport.test.js
  92. 39 10
      yarn.lock

+ 5 - 2
CHANGES.md

@@ -1,8 +1,11 @@
 # CHANGES
 
-## v3.6.10
+## v3.7.0-RC
+
+* Feature: [Draw.io](https://www.draw.io/) Integration
+* Feature: SAML Attribute-based Login Control
+* Improvement: Reactify admin pages (Security)
 
-*
 
 ## v3.6.9
 

+ 4 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.6.9-RC",
+  "version": "3.7.0-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -109,6 +109,7 @@
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "is-iso-date": "^0.0.1",
+    "lucene-query-parser": "^1.2.0",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "migrate-mongo": "^7.0.1",
@@ -191,7 +192,8 @@
     "load-css-file": "^1.0.0",
     "lodash-webpack-plugin": "^0.11.5",
     "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.0.2",
+    "markdown-it-blockdiag": "^1.0.3",
+    "markdown-it-drawio-viewer": "^1.1.3",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",

+ 8 - 0
resource/cdn-manifests.js

@@ -79,6 +79,14 @@ module.exports = {
         integrity: '',
       },
     },
+    {
+      name: 'drawio-viewer',
+      url: 'https://jgraph.github.io/drawio/src/main/webapp/js/viewer.min.js',
+      groups: ['basis'],
+      args: {
+        integrity: '',
+      },
+    },
   ],
   style: [
     {

Разница между файлами не показана из-за своего большого размера
+ 7 - 0
resource/locales/en-US/sandbox-diagrams.md


+ 71 - 0
resource/locales/en-US/sandbox-math.md

@@ -0,0 +1,71 @@
+# :pencil: Math
+
+See [MathJax](https://www.mathjax.org/).
+
+## Inline Formula
+
+When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+
+## The Lorenz Equations
+
+$$
+\begin{align}
+\dot{x} & = \sigma(y-x) \\
+\dot{y} & = \rho x - y - xz \\
+\dot{z} & = -\beta z + xy
+\end{align}
+$$
+
+
+## The Cauchy-Schwarz Inequality
+
+$$
+\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
+ \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+$$
+
+## A Cross Product Formula
+
+$$
+\mathbf{V}_1 \times \mathbf{V}_2 =
+ \begin{vmatrix}
+  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
+ \end{vmatrix}
+$$
+
+
+## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
+
+$$
+P(E) = {n \choose k} p^k (1-p)^{ n-k}
+$$
+
+## An Identity of Ramanujan
+
+$$
+\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
+     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+$$
+
+## A Rogers-Ramanujan Identity
+
+$$
+1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+     \quad\quad \text{for $|q|<1$}.
+$$
+
+## Maxwell's Equations
+
+$$
+\begin{align}
+  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
+  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+  \nabla \cdot \vec{\mathbf{B}} & = 0
+\end{align}
+$$

+ 22 - 358
resource/locales/en-US/sandbox.md

@@ -28,8 +28,11 @@
 ```
 
 ### Header 3
+
 #### Header 4
+
 ##### Header 5
+
 ###### Header 6
 
 ## Block 段落
@@ -48,7 +51,7 @@
 
 ## Br 改行
 
-改行の前に半角スペース`  `を2つ記述します。
+改行の前に半角スペース``を2つ記述します。
 ***この挙動は、オプションで変更可能です***
 
 ```
@@ -159,6 +162,7 @@ ___
 # :pencil: Typography
 
 ## 強調
+
 ### em
 
 アスタリスク`*`もしくはアンダースコア`_`1個で文字列を囲みます。
@@ -353,30 +357,30 @@ aligned    | aligned     | aligned
 
 ```
 ::: tsv
-Content Cell 	Content Cell
-Content Cell 	Content Cell
+Content Cell  Content Cell
+Content Cell  Content Cell
 :::
 ```
 
 ::: tsv
-Content Cell	Content Cell
-Content Cell	Content Cell
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## TSV ヘッダ付き (crowi-plus 独自記法)
 
 ```
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 ```
 
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## CSV (crowi-plus 独自記法)
@@ -442,351 +446,11 @@ See [emojione](https://www.emojione.com/)
 :watch: :gear: :gem: :wrench: :envelope:
 
 
-# :pencil: Math
-
-See [MathJax](https://www.mathjax.org/).
-
-## Inline Formula
-
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
-  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
-
-## The Lorenz Equations
-
-$$
-\begin{align}
-\dot{x} & = \sigma(y-x) \\
-\dot{y} & = \rho x - y - xz \\
-\dot{z} & = -\beta z + xy
-\end{align}
-$$
-
-
-## The Cauchy-Schwarz Inequality
-
-$$
-\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
- \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
-$$
-
-## A Cross Product Formula
-
-$$
-\mathbf{V}_1 \times \mathbf{V}_2 =
- \begin{vmatrix}
-  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
-  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
-  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
- \end{vmatrix}
-$$
-
-
-## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
-
-$$
-P(E) = {n \choose k} p^k (1-p)^{ n-k}
-$$
-
-## An Identity of Ramanujan
-
-$$
-\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
-     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
-      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
-$$
-
-## A Rogers-Ramanujan Identity
-
-$$
-1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
-    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
-     \quad\quad \text{for $|q|<1$}.
-$$
-
-## Maxwell's Equations
-
-$$
-\begin{align}
-  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
-  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
-  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
-  \nabla \cdot \vec{\mathbf{B}} & = 0
-\end{align}
-$$
-
-<!-- Reset MathJax -->
-<div class="clearfix"></div>
-
-
-# :pencil: UML Diagrams
-
-See [PlantUML](http://plantuml.com/).
-
-## シーケンス図
-
-@startuml
-skinparam sequenceArrowThickness 2
-skinparam roundcorner 20
-skinparam maxmessagesize 60
-skinparam sequenceParticipant underline
-
-actor User
-participant "First Class" as A
-participant "Second Class" as B
-participant "Last Class" as C
-
-User -> A: DoWork
-activate A
-
-A -> B: Create Request
-activate B
+# :heavy_plus_sign: More..
 
-B -> C: DoWork
-activate C
-C --> B: WorkDone
-destroy C
-
-B --> A: Request Created
-deactivate B
-
-A --> User: Done
-deactivate A
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## クラス図
-
-@startuml
-
-class BaseClass
-
-namespace net.dummy #DDDDDD {
-    .BaseClass <|-- Person
-    Meeting o-- Person
-
-    .BaseClass <|- Meeting
-}
-
-namespace net.foo {
-  net.dummy.Person  <|- Person
-  .BaseClass <|-- Person
-
-  net.dummy.Meeting o-- Person
-}
-
-BaseClass <|-- net.unused.Person
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## コンポーネント図
-
-@startuml
-
-package "Some Group" {
-  HTTP - [First Component]
-  [Another Component]
-}
-
-node "Other Groups" {
-  FTP - [Second Component]
-  [First Component] --> FTP
-}
-
-cloud {
-  [Example 1]
-}
-
-
-database "MySql" {
-  folder "This is my folder" {
-    [Folder 3]
-  }
-  frame "Foo" {
-    [Frame 4]
-  }
-}
-
-
-[Another Component] --> [Example 1]
-[Example 1] --> [Folder 3]
-[Folder 3] --> [Frame 4]
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## ステート図
-
-
-@startuml
-scale 600 width
-
-[*] -> State1
-State1 --> State2 : Succeeded
-State1 --> [*] : Aborted
-State2 --> State3 : Succeeded
-State2 --> [*] : Aborted
-state State3 {
-  state "Accumulate Enough Data\nLong State Name" as long1
-  long1 : Just a test
-  [*] --> long1
-  long1 --> long1 : New Data
-  long1 --> ProcessData : Enough Data
-}
-State3 --> State3 : Failed
-State3 --> [*] : Succeeded / Save Result
-State3 --> [*] : Aborted
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-# :pencil: blockdiag
-
-See [blockdiag](http://blockdiag.com/).
-
-## blockdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: blockdiag
-blockdiag {
-   A -> B -> C -> D;
-   A -> E -> F -> G;
-}
-:::
-
-</div>
-
-## seqdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: seqdiag
-seqdiag {
-  browser  -> webserver [label = "GET /index.html"];
-  browser <-- webserver;
-  browser  -> webserver [label = "POST /blog/comment"];
-              webserver  -> database [label = "INSERT comment"];
-              webserver <-- database;
-  browser <-- webserver;
-}
-:::
-
-</div>
-
-## actdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: actdiag
-actdiag {
-  write -> convert -> image
-
-  lane user {
-     label = "User"
-     write [label = "Writing reST"];
-     image [label = "Get diagram IMAGE"];
-  }
-  lane actdiag {
-     convert [label = "Convert reST to Image"];
-  }
-}
-:::
-
-</div>
-
-## nwdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: nwdiag
-nwdiag {
-  network dmz {
-      address = "210.x.x.x/24"
-
-      web01 [address = "210.x.x.1"];
-      web02 [address = "210.x.x.2"];
-  }
-  network internal {
-      address = "172.x.x.x/24";
-
-      web01 [address = "172.x.x.1"];
-      web02 [address = "172.x.x.2"];
-      db01;
-      db02;
-  }
-}
-:::
-
-</div>
-
-## rackdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: rackdiag
-rackdiag {
-  // define height of rack
-  8U;
-
-  // define rack items
-  1: UPS [2U];
-  3: DB Server
-  4: Web Server
-  5: Web Server
-  6: Web Server
-  7: Load Balancer
-  8: L3 Switch
-}
-:::
-
-</div>
-
-## packetdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: packetdiag
-packetdiag {
-  colwidth = 32
-  node_height = 72
-
-  0-15: Source Port
-  16-31: Destination Port
-  32-63: Sequence Number
-  64-95: Acknowledgment Number
-  96-99: Data Offset
-  100-105: Reserved
-  106: URG [rotate = 270]
-  107: ACK [rotate = 270]
-  108: PSH [rotate = 270]
-  109: RST [rotate = 270]
-  110: SYN [rotate = 270]
-  111: FIN [rotate = 270]
-  112-127: Window
-  128-143: Checksum
-  144-159: Urgent Pointer
-  160-191: (Options and Padding)
-  192-223: data [colheight = 3]
-}
-:::
-
-</div>
+- Try to attach Bootstrap3 Tags?
+    - :arrow_right: [/Sandbox/Bootstrap3]
+- Try to draw Diagrams?
+    - :arrow_right: [/Sandbox/Diagrams]
+- Try to write Math Formulas?
+    - :arrow_right: [/Sandbox/Math]

+ 55 - 36
resource/locales/en-US/translation.json

@@ -124,8 +124,9 @@
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
   "form_validation": {
-    "required": "<code>%s</code> is required",
-    "invalid_syntax": "The syntax of <code>%s</code> is invalid."
+    "error_message": "Some values ​​are incorrect",
+    "required": "%s is required",
+    "invalid_syntax": "The syntax of %s is invalid."
   },
   "installer": {
     "setup": "Setup",
@@ -388,16 +389,19 @@
     "admin_and_author": "Admin and Author",
     "anyone": "Anyone",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
-    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the %s",
+    "setup_is_not_yet_complete": "Setup is not yet complete",
+    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting_link": "Go to Markdown settings",
     "callback_URL": "Callback URL",
     "providerName": "Provider Name",
     "issuerHost": "Issuer Host",
     "scope": "Scope",
-    "desc_of_callback_URL": "Use it in the setting of the %s provider",
+    "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
     "clientID": "Client ID",
     "client_secret": "Client Secret",
+    "updated_general_security_setting": "Succeeded to update security setting",
+    "setup_not_completed_yet": "Setup not completed yet",
     "guest_mode": {
       "deny": "Deny (Registered Users Only)",
       "readonly": "Accept (Guests can read only)"
@@ -409,17 +413,19 @@
     },
     "configuration": " Configuration",
     "optional": "Optional",
-    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>%s</code> match",
-    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>%s</code>.",
-    "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>%s</code> match",
-    "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>%s</code>.",
-    "Use env var if empty": "Use env var <code>%s</code> if empty",
-    "Use default if both are empty": "If both ​​are empty, the default value <code>%s</code> is used.",
+    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
+    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
+    "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>email</code> match",
+    "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>email</code>.",
+    "Use env var if empty": "Use env var <code>{{env}}</code> if empty",
+    "Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
     "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
     "Local": {
-      "name": "ID/Password"
+      "name": "ID/Password",
+      "enable_local": "enable ID/Password"
     },
     "ldap": {
+      "enable_ldap": "enable LDAP",
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "bind_mode": "Binding Mode",
       "bind_manager": "Manager Bind",
@@ -448,52 +454,64 @@
       "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
       "group_search_user_DN_property": "User DN Property",
       "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
-      "test_config": "Test Saved Configuration"
+      "test_config": "Test Saved Configuration",
+      "updated_ldap": "Succeeded to update LDAP setting"
     },
     "SAML": {
       "name": "SAML",
+      "enable_saml": "enable SAML",
       "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
-      "mapping_detail": "Specification of mappings for %s when creating new users",
+      "mapping_detail": "Specification of mappings for {{target}} when creating new users",
       "cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
-      "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>%s</code> is used.",
+      "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
       "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>%s</code> .",
       "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
-      "attr_based_login_control_rule_detail": "A rule is written in the form of concatenating <code>attribute name = value</code> with <code>|</code> and <code>&</code>. The or operator (|) has a lower precedence than the and operator (&). If operator precedence is equal, left to right associativity is used.",
-      "attr_based_login_control_rule_example": "For example, if a rule is <code>Department = A | Department = B & Position = Leader</code>, users with <code>Department</code> as <code>A</code> or users with <code>Department</code> as <code>B</code> and <code>Position</code> as <code>Leader</code> <strong>can</strong> sign in."
+      "attr_based_login_control_rule_detail": "See <a href=\"https://lucene.apache.org/core/2_9_4/queryparsersyntax.html\" target=\"_blank\">Apache Lucene - Query Parser Syntax</a>.<h6>Supported Queries:</h6><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h6>Unsupported Queries:</h6><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul>",
+      "attr_based_login_control_rule_example": "<h6>Example</h6>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
+      "updated_saml": "Succeeded to update SAML setting"
     },
     "Basic": {
+      "enable_basic": "enable Basic",
       "name": "Basic Authentication",
       "desc_1": "Login with <code>username</code> in Authorization header.",
-      "desc_2": "User will be automatically generated if not exist."
+      "desc_2": "User will be automatically generated if not exist.",
+      "updated_basic": "Succeeded to update Basic setting"
     },
     "OAuth": {
+      "enable_oidc": "enable OIDC",
       "register": "Register for %s",
       "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
       "Google": {
+        "enable_google": "enable Google OAuth",
         "name": "Google OAuth",
-        "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
+        "register_1": "Access {{link}}",
         "register_2": "Create Project if no projects exist",
         "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>%s</code>",
-        "register_5": "Copy and paste your ClientID and Client Secret above"
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
+        "register_5": "Copy and paste your ClientID and Client Secret above",
+        "updated_google": "Succeeded to update Google OAuth setting"
       },
       "Facebook": {
         "name": "Facebook OAuth"
       },
       "Twitter": {
+        "enable_twitter": "enable Twitter OAuth",
         "name": "Twitter OAuth",
-        "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
+        "register_1": "Access {{link}}",
         "register_2": "Sign in Twitter",
         "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>%s</code>",
-        "register_5": "Copy and paste your ClientID and Client Secret above"
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
+        "register_5": "Copy and paste your ClientID and Client Secret above",
+        "updated_twitter": "Succeeded to update Twitter OAuth setting"
       },
       "GitHub": {
+        "enable_github": "enable GitHub OAuth",
         "name": "GitHub OAuth",
-        "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
-        "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>%s</code>",
-        "register_3": "Copy and paste your ClientID and Client Secret above"
+        "register_1": "Access {{link}}",
+        "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_github": "Succeeded to update GitHub OAuth setting"
       },
       "OIDC": {
         "name": "OpenID Connect",
@@ -503,7 +521,8 @@
         "mapping_detail": "Specification of mappings for %s when creating new users",
         "register_1": "Contant to OIDC IdP Administrator",
         "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
-        "register_3": "Copy and paste your ClientID and Client Secret above"
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_oidc": "Succeeded to update OpenID Connect"
       },
       "how_to": {
         "google": "How to configure Google OAuth?",
@@ -513,15 +532,15 @@
       }
     },
     "form_item_name": {
-      "security:passport-saml:entryPoint": "Entry point",
-      "security:passport-saml:issuer": "Issuer",
-      "security:passport-saml:cert": "Certificate",
-      "security:passport-saml:attrMapId": "ID",
-      "security:passport-saml:attrMapUsername": "Username",
-      "security:passport-saml:attrMapMail": "Mail Address",
-      "security:passport-saml:attrMapFirstName": "First Name",
-      "security:passport-saml:attrMapLastName": "Last Name",
-      "security:passport-saml:ABLCRule": "Rule"
+      "entryPoint": "Entry point",
+      "issuer": "Issuer",
+      "cert": "Certificate",
+      "attrMapId": "ID",
+      "attrMapUsername": "Username",
+      "attrMapMail": "Mail Address",
+      "attrMapFirstName": "First Name",
+      "attrMapLastName": "Last Name",
+      "ABLCRule": "Rule"
     }
   },
   "notification_setting": {

Разница между файлами не показана из-за своего большого размера
+ 7 - 0
resource/locales/ja/sandbox-diagrams.md


+ 71 - 0
resource/locales/ja/sandbox-math.md

@@ -0,0 +1,71 @@
+# :pencil: Math
+
+See [MathJax](https://www.mathjax.org/).
+
+## Inline Formula
+
+When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+
+## The Lorenz Equations
+
+$$
+\begin{align}
+\dot{x} & = \sigma(y-x) \\
+\dot{y} & = \rho x - y - xz \\
+\dot{z} & = -\beta z + xy
+\end{align}
+$$
+
+
+## The Cauchy-Schwarz Inequality
+
+$$
+\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
+ \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+$$
+
+## A Cross Product Formula
+
+$$
+\mathbf{V}_1 \times \mathbf{V}_2 =
+ \begin{vmatrix}
+  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
+ \end{vmatrix}
+$$
+
+
+## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
+
+$$
+P(E) = {n \choose k} p^k (1-p)^{ n-k}
+$$
+
+## An Identity of Ramanujan
+
+$$
+\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
+     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+$$
+
+## A Rogers-Ramanujan Identity
+
+$$
+1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+     \quad\quad \text{for $|q|<1$}.
+$$
+
+## Maxwell's Equations
+
+$$
+\begin{align}
+  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
+  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+  \nabla \cdot \vec{\mathbf{B}} & = 0
+\end{align}
+$$

+ 22 - 357
resource/locales/ja/sandbox.md

@@ -28,8 +28,11 @@
 ```
 
 ### 見出し3
+
 #### 見出し4
+
 ##### 見出し5
+
 ###### 見出し6
 
 ## Block 段落
@@ -48,7 +51,7 @@
 
 ## Br 改行
 
-改行の前に半角スペース`  `を2つ記述します。
+改行の前に半角スペース``を2つ記述します。
 ***この挙動は、オプションで変更可能です***
 
 ```
@@ -159,6 +162,7 @@ ___
 # :pencil: Typography
 
 ## 強調
+
 ### em
 
 アスタリスク`*`もしくはアンダースコア`_`1個で文字列を囲みます。
@@ -353,30 +357,30 @@ aligned    | aligned     | aligned
 
 ```
 ::: tsv
-Content Cell 	Content Cell
-Content Cell 	Content Cell
+Content Cell  Content Cell
+Content Cell  Content Cell
 :::
 ```
 
 ::: tsv
-Content Cell	Content Cell
-Content Cell	Content Cell
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## TSV ヘッダ付き (crowi-plus 独自記法)
 
 ```
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 ```
 
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## CSV (crowi-plus 独自記法)
@@ -442,351 +446,12 @@ See [emojione](https://www.emojione.com/)
 :watch: :gear: :gem: :wrench: :envelope:
 
 
-# :pencil: Math
-
-See [MathJax](https://www.mathjax.org/).
-
-## Inline Formula
-
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
-  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
-
-## The Lorenz Equations
-
-$$
-\begin{align}
-\dot{x} & = \sigma(y-x) \\
-\dot{y} & = \rho x - y - xz \\
-\dot{z} & = -\beta z + xy
-\end{align}
-$$
-
-
-## The Cauchy-Schwarz Inequality
-
-$$
-\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
- \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
-$$
-
-## A Cross Product Formula
-
-$$
-\mathbf{V}_1 \times \mathbf{V}_2 =
- \begin{vmatrix}
-  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
-  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
-  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
- \end{vmatrix}
-$$
-
-
-## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
-
-$$
-P(E) = {n \choose k} p^k (1-p)^{ n-k}
-$$
-
-## An Identity of Ramanujan
-
-$$
-\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
-     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
-      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
-$$
-
-## A Rogers-Ramanujan Identity
-
-$$
-1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
-    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
-     \quad\quad \text{for $|q|<1$}.
-$$
-
-## Maxwell's Equations
-
-$$
-\begin{align}
-  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
-  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
-  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
-  \nabla \cdot \vec{\mathbf{B}} & = 0
-\end{align}
-$$
-
-<!-- Reset MathJax -->
-<div class="clearfix"></div>
-
-
-# :pencil: UML Diagrams
-
-See [PlantUML](http://plantuml.com/).
-
-## シーケンス図
-
-@startuml
-skinparam sequenceArrowThickness 2
-skinparam roundcorner 20
-skinparam maxmessagesize 60
-skinparam sequenceParticipant underline
-
-actor User
-participant "First Class" as A
-participant "Second Class" as B
-participant "Last Class" as C
-
-User -> A: DoWork
-activate A
-
-A -> B: Create Request
-activate B
-
-B -> C: DoWork
-activate C
-C --> B: WorkDone
-destroy C
-
-B --> A: Request Created
-deactivate B
-
-A --> User: Done
-deactivate A
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## クラス図
-
-@startuml
-
-class BaseClass
-
-namespace net.dummy #DDDDDD {
-    .BaseClass <|-- Person
-    Meeting o-- Person
-
-    .BaseClass <|- Meeting
-}
-
-namespace net.foo {
-  net.dummy.Person  <|- Person
-  .BaseClass <|-- Person
-
-  net.dummy.Meeting o-- Person
-}
-
-BaseClass <|-- net.unused.Person
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## コンポーネント図
-
-@startuml
-
-package "Some Group" {
-  HTTP - [First Component]
-  [Another Component]
-}
-
-node "Other Groups" {
-  FTP - [Second Component]
-  [First Component] --> FTP
-}
-
-cloud {
-  [Example 1]
-}
-
-
-database "MySql" {
-  folder "This is my folder" {
-    [Folder 3]
-  }
-  frame "Foo" {
-    [Frame 4]
-  }
-}
-
-
-[Another Component] --> [Example 1]
-[Example 1] --> [Folder 3]
-[Folder 3] --> [Frame 4]
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## ステート図
-
-
-@startuml
-scale 600 width
-
-[*] -> State1
-State1 --> State2 : Succeeded
-State1 --> [*] : Aborted
-State2 --> State3 : Succeeded
-State2 --> [*] : Aborted
-state State3 {
-  state "Accumulate Enough Data\nLong State Name" as long1
-  long1 : Just a test
-  [*] --> long1
-  long1 --> long1 : New Data
-  long1 --> ProcessData : Enough Data
-}
-State3 --> State3 : Failed
-State3 --> [*] : Succeeded / Save Result
-State3 --> [*] : Aborted
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-# :pencil: blockdiag
-
-See [blockdiag](http://blockdiag.com/).
-
-## blockdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: blockdiag
-blockdiag {
-   A -> B -> C -> D;
-   A -> E -> F -> G;
-}
-:::
-
-</div>
-
-## seqdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: seqdiag
-seqdiag {
-  browser  -> webserver [label = "GET /index.html"];
-  browser <-- webserver;
-  browser  -> webserver [label = "POST /blog/comment"];
-              webserver  -> database [label = "INSERT comment"];
-              webserver <-- database;
-  browser <-- webserver;
-}
-:::
-
-</div>
-
-## actdiag
 
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
+# :heavy_plus_sign: 更に…
 
-::: actdiag
-actdiag {
-  write -> convert -> image
-
-  lane user {
-     label = "User"
-     write [label = "Writing reST"];
-     image [label = "Get diagram IMAGE"];
-  }
-  lane actdiag {
-     convert [label = "Convert reST to Image"];
-  }
-}
-:::
-
-</div>
-
-## nwdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: nwdiag
-nwdiag {
-  network dmz {
-      address = "210.x.x.x/24"
-
-      web01 [address = "210.x.x.1"];
-      web02 [address = "210.x.x.2"];
-  }
-  network internal {
-      address = "172.x.x.x/24";
-
-      web01 [address = "172.x.x.1"];
-      web02 [address = "172.x.x.2"];
-      db01;
-      db02;
-  }
-}
-:::
-
-</div>
-
-## rackdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: rackdiag
-rackdiag {
-  // define height of rack
-  8U;
-
-  // define rack items
-  1: UPS [2U];
-  3: DB Server
-  4: Web Server
-  5: Web Server
-  6: Web Server
-  7: Load Balancer
-  8: L3 Switch
-}
-:::
-
-</div>
-
-## packetdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: packetdiag
-packetdiag {
-  colwidth = 32
-  node_height = 72
-
-  0-15: Source Port
-  16-31: Destination Port
-  32-63: Sequence Number
-  64-95: Acknowledgment Number
-  96-99: Data Offset
-  100-105: Reserved
-  106: URG [rotate = 270]
-  107: ACK [rotate = 270]
-  108: PSH [rotate = 270]
-  109: RST [rotate = 270]
-  110: SYN [rotate = 270]
-  111: FIN [rotate = 270]
-  112-127: Window
-  128-143: Checksum
-  144-159: Urgent Pointer
-  160-191: (Options and Padding)
-  192-223: data [colheight = 3]
-}
-:::
-
-</div>
+- Bootstrap3 のタグを使う
+    - :arrow_right: [/Sandbox/Bootstrap3]
+- 図表を書く
+    - :arrow_right: [/Sandbox/Diagrams]
+- 数式を書く
+    - :arrow_right: [/Sandbox/Math]

+ 61 - 36
resource/locales/ja/translation.json

@@ -123,8 +123,9 @@
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
   "form_validation": {
-    "required": "<code>%s</code> に値を入力してください",
-    "invalid_syntax": "<code>%s</code> の構文が不正です"
+    "error_message": "いくつかの値が設定されていません",
+    "required": "%sに値を入力してください",
+    "invalid_syntax": "%sの構文が不正です"
   },
   "installer": {
     "setup": "セットアップ",
@@ -365,7 +366,7 @@
   },
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
-    "Fixed by env var": "環境変数 <code>%s=%s</code> により固定されています。",
+    "Fixed by env var": "環境変数 <code>{{forcewikimode}}={{wikimode}}</code> により固定されています。",
     "Register limitation": "登録の制限",
     "Register limitation desc": "新しいユーザーを登録する方法を制限します.",
     "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
@@ -385,13 +386,16 @@
     "admin_and_author": "管理者とページ作者が可能",
     "anyone": "誰でも可能",
     "Authentication mechanism settings": "認証機構設定",
-    "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。%s から設定してください。",
+    "setup_is_not_yet_complete":"セットアップはまだ完了してません",
+    "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
-    "desc_of_callback_URL": "%s プロバイダ側の設定で利用してください。",
+    "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "clientID": "クライアントID",
     "client_secret": "クライアントシークレット",
+    "updated_general_security_setting": "セキュリティ設定を更新しました。",
+    "setup_not_completed_yet": "まだセットアップは完了していません。",
     "guest_mode": {
       "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
       "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"
@@ -403,17 +407,19 @@
     },
     "configuration": "設定",
     "optional": "オプション",
-    "Treat username matching as identical": "新規ログイン時、<code>%s</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
-    "Treat username matching as identical_warn": "警告: <code>%s</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
-    "Treat email matching as identical": "新規ログイン時、<code>%s</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
-    "Treat email matching as identical_warn": "警告: <code>%s</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
-    "Use env var if empty": "空の場合、環境変数 <code>%s</code> を利用します",
-    "Use default if both are empty": "どちらの値も空の場合、デフォルト値 <code>%s</code> を利用します",
+    "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
+    "Treat username matching as identical_warn": "警告: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
+    "Treat email matching as identical": "新規ログイン時、<code>email</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
+    "Treat email matching as identical_warn": "警告: <code>email</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
+    "Use env var if empty": "空の場合、環境変数 <code>{{env}}</code> を利用します",
+    "Use default if both are empty": "どちらの値も空の場合、デフォルト値 <code>{{target}}</code> を利用します",
     "missing mandatory configs": "以下の必須項目の値がデータベースと環境変数のどちらにも設定されていません",
     "Local": {
-      "name": "ID/Password"
+      "name": "ID/Password",
+      "enable_local": "ID/Password を有効にする"
     },
     "ldap": {
+      "enable_ldap": "LDAP を有効にする",
       "server_url_detail": "LDAP URLを <code>ldap://host:port/DN</code> または <code>ldaps://host:port/DN</code> の形式で入力してください。",
       "bind_mode": "Bind モード",
       "bind_manager": "管理者 Bind",
@@ -442,52 +448,71 @@
       "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> は <code>cn=group1</code> と、ユーザーの <code>uid</code> を含む <code>memberUid</code> を持つグループにヒットします(<code>ユーザーの DN プロパティー</code> がデフォルトから変更されていない場合)",
       "group_search_user_DN_property": "ユーザーの DN プロパティー",
       "group_search_user_DN_property_detail": "<code>グループ検索フィルター</code> 内の <code>&#123;&#123;dn&#125;&#125;</code> で置換される、ユーザーオブジェクトのプロパティー",
-      "test_config": "ログインテスト"
+      "test_config": "ログインテスト",
+      "updated_ldap": "LDAP設定 を更新しました"
     },
     "SAML": {
       "name": "SAML",
+      "enable_saml": "SAML を有効にする",
       "id_detail": "SAML Identity プロバイダ内で一意に識別可能な値を格納している属性",
       "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
-      "mapping_detail": "新規ユーザーの%sに関連付ける属性",
+      "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
       "cert_detail": "IdP からのレスポンスの validation を行うためのPEMエンコードされた X.509 証明書",
-      "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>%s</code> の値を利用します",
-      "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>%s</code> の値をfalseに変更もしくは削除してください",
+      "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します",
+      "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
       "attr_based_login_control_detail": "SAMLの <code>&lt;saml:AttributeStatement&gt;</code> 要素に含まれる <code>&lt;saml:Attribute&gt;</code> 要素と、その子要素 <code>&lt;saml:AttributeValue&gt;</code> を利用してログインの可否を制御します。",
-      "attr_based_login_control_rule_detail": "<code>属性名 = 値</code> を <code>|</code>(論理和)、 <code>&</code>(論理積) で連結した形式で記述してください。演算子の優先順位は論理積が論理和より高く、各演算子の結合規則は左から右です。",
-      "attr_based_login_control_rule_example": "例えば <code>Department=A | Department=B & Position=Leader</code> だと <code>Department</code> が <code>A</code> の場合, もしくは<code>Department</code> が <code>B</code> かつ <code>Position</code> が <code>Leader</code> の場合にログインを<strong>許可</strong>します。"
+      "attr_based_login_control_rule_detail": "See <a href=\"https://lucene.apache.org/core/2_9_4/queryparsersyntax.html\" target=\"_blank\">Apache Lucene - Query Parser Syntax</a>.<h6>利用可能なクエリ:</h6><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h6>利用不可なクエリ:</h6><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul>",
+      "attr_based_login_control_rule_example": "<h6>Example</h6>ルールに <code>(Department: A || Department: B) && Position: Leader</code> を指定した場合, <code>Department: A</code> または <code>Department: B</code> のどちらかに該当し、かつ <code>Position: Leader</code> を持つユーザーにログインを<strong>許可</strong>します。"
     },
     "Basic": {
+      "enable_basic": "Basic を有効にする",
       "name": "Basic 認証",
       "desc_1": "Authorization ヘッダに格納されている <code>username</code> でログインします。",
-      "desc_2": "ユーザーが存在しなかった場合は自動生成します。"
+      "desc_2": "ユーザーが存在しなかった場合は自動生成します。",
+      "updated_basic": "Basic認証 を更新しました"
     },
     "OAuth": {
+      "enable_oidc": "OIDC を有効にする",
       "register": "%sに登録",
       "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力",
       "Google": {
+        "enable_google": "Google OAuth を有効にする",
         "name": "Google OAuth",
-        "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
+        "register_1": "{{link}}へアクセス",
         "register_2": "プロジェクトがない場合はプロジェクトを作成",
         "register_3": "認証情報を作成 &rightarrow; OAuthクライアントID &rightarrow; ウェブアプリケーションを選択",
-        "register_4": "承認済みのリダイレクトURIを<code>%s</code>としてGrowiを登録",
-        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力"
+        "register_4": "承認済みのリダイレクトURIを<code>{{url}}</code>としてGrowiを登録",
+        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
+        "updated_google": "Google OAuth を更新しました"
       },
       "Facebook": {
         "name": "Facebook OAuth"
       },
       "Twitter": {
+        "enable_twitter": "Twitter OAuth を有効にする",
         "name": "Twitter OAuth",
-        "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
+        "register_1": "{{link}} へアクセス",
         "register_2": "Twitterにサインイン",
         "register_3": "Create New Appをクリック &rightarrow; Application Detailsの各項目を入力",
         "register_4": "Create your Twitter Applicationで作成",
-        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力"
+        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
+        "updated_twitter": "Twitter OAuth を更新しました"
       },
       "GitHub": {
+        "enable_github": "GitHub OAuth を有効にする",
         "name": "GitHub OAuth",
-        "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
-        "register_2": "\"Authorization callback URL\"を<code>%s</code>としてGrowiを登録",
-        "register_3": "上記フォームにクライアントIDとクライアントシークレットを入力"
+        "register_1": "{{link}} へアクセス",
+        "register_2": "\"Authorization callback URL\"を<code>{{url}}</code>としてGrowiを登録",
+        "register_3": "上記フォームにクライアントIDとクライアントシークレットを入力",
+        "updated_github": "GitHub OAuth を更新しました"
+      },
+      "OIDC": {
+        "name": "OpenID Connect",
+        "id_detail": "OIDC claims で一意に識別可能な値を格納している属性",
+        "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
+        "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
+        "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
+        "updated_oidc": "OpenID Connect を更新しました"
       },
       "how_to": {
         "google": "Google OAuth の設定方法",
@@ -496,15 +521,15 @@
       }
     },
     "form_item_name": {
-      "security:passport-saml:entryPoint": "エントリーポイント",
-      "security:passport-saml:issuer": "発行者",
-      "security:passport-saml:cert": "証明書",
-      "security:passport-saml:attrMapId": "ID",
-      "security:passport-saml:attrMapUsername": "ユーザー名",
-      "security:passport-saml:attrMapMail": "メールアドレス",
-      "security:passport-saml:attrMapFirstName": "姓",
-      "security:passport-saml:attrMapLastName": "名",
-      "security:passport-saml:ABLCRule": "ルール"
+      "entryPoint": "エントリーポイント",
+      "issuer": "発行者",
+      "cert": "証明書",
+      "attrMapId": "ID",
+      "attrMapUsername": "ユーザー名",
+      "attrMapMail": "メールアドレス",
+      "attrMapFirstName": "姓",
+      "attrMapLastName": "名",
+      "ABLCRule": "ルール"
     }
   },
   "notification_setting": {

+ 35 - 0
src/client/js/admin.jsx

@@ -12,6 +12,7 @@ import ManageGlobalNotification from './components/Admin/Notification/ManageGlob
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import UserManagement from './components/Admin/UserManagement';
 import AppSettingsPage from './components/Admin/App/AppSettingsPage';
+import SecurityManagement from './components/Admin/Security/SecurityManagement';
 import ManageExternalAccount from './components/Admin/ManageExternalAccount';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
 import Customize from './components/Admin/Customize/Customize';
@@ -27,6 +28,15 @@ import AdminUsersContainer from './services/AdminUsersContainer';
 import AdminAppContainer from './services/AdminAppContainer';
 import AdminMarkDownContainer from './services/AdminMarkDownContainer';
 import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
+import AdminGeneralSecurityContainer from './services/AdminGeneralSecurityContainer';
+import AdminLdapSecurityContainer from './services/AdminLdapSecurityContainer';
+import AdminLocalSecurityContainer from './services/AdminLocalSecurityContainer';
+import AdminSamlSecurityContainer from './services/AdminSamlSecurityContainer';
+import AdminOidcSecurityContainer from './services/AdminOidcSecurityContainer';
+import AdminBasicSecurityContainer from './services/AdminBasicSecurityContainer';
+import AdminGoogleSecurityContainer from './services/AdminGoogleSecurityContainer';
+import AdminGitHubSecurityContainer from './services/AdminGitHubSecurityContainer';
+import AdminTwitterSecurityContainer from './services/AdminTwitterSecurityContainer';
 import AdminNotificationContainer from './services/AdminNotificationContainer';
 
 import { appContainer, componentMappings } from './bootstrap';
@@ -97,3 +107,28 @@ Object.keys(componentMappings).forEach((key) => {
     );
   }
 });
+
+const adminSecuritySettingElem = document.getElementById('admin-security-setting');
+if (adminSecuritySettingElem != null) {
+  const adminGeneralSecurityContainer = new AdminGeneralSecurityContainer(appContainer);
+  const adminLocalSecurityContainer = new AdminLocalSecurityContainer(appContainer);
+  const adminLdapSecurityContainer = new AdminLdapSecurityContainer(appContainer);
+  const adminSamlSecurityContainer = new AdminSamlSecurityContainer(appContainer);
+  const adminOidcSecurityContainer = new AdminOidcSecurityContainer(appContainer);
+  const adminBasicSecurityContainer = new AdminBasicSecurityContainer(appContainer);
+  const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer(appContainer);
+  const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer(appContainer);
+  const adminTwitterSecurityContainer = new AdminTwitterSecurityContainer(appContainer);
+  const adminSecurityContainers = [
+    adminGeneralSecurityContainer, adminLocalSecurityContainer, adminLdapSecurityContainer, adminSamlSecurityContainer,
+    adminOidcSecurityContainer, adminBasicSecurityContainer, adminGoogleSecurityContainer, adminGitHubSecurityContainer, adminTwitterSecurityContainer,
+  ];
+  ReactDOM.render(
+    <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
+      <I18nextProvider i18n={i18n}>
+        <SecurityManagement />
+      </I18nextProvider>
+    </Provider>,
+    adminSecuritySettingElem,
+  );
+}

+ 1 - 1
src/client/js/components/Admin/App/AppSettingsPage.jsx

@@ -27,7 +27,7 @@ class AppSettingsPage extends React.Component {
     }
     catch (err) {
       toastError(err);
-      adminAppContainer.setState({ retrieveError: err });
+      adminAppContainer.setState({ retrieveError: err.message });
       logger.error(err);
     }
   }

+ 1 - 1
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -26,7 +26,7 @@ class MarkdownSetting extends React.Component {
     }
     catch (err) {
       toastError(err);
-      adminMarkDownContainer.setState({ retrieveError: err });
+      adminMarkDownContainer.setState({ retrieveError: err.message });
       logger.error(err);
     }
 

+ 147 - 0
src/client/js/components/Admin/Security/BasicSecuritySetting.jsx

@@ -0,0 +1,147 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminBasicSecurityContainer from '../../../services/AdminBasicSecurityContainer';
+
+class BasicSecurityManagement extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+    };
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminBasicSecurityContainer } = this.props;
+
+    try {
+      await adminBasicSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+  async onClickSubmit() {
+    const { t, adminBasicSecurityContainer, adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminBasicSecurityContainer.updateBasicSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.Basic.updated_basic'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminBasicSecurityContainer } = this.props;
+    const { isBasicEnabled } = adminGeneralSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+    return (
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          { t('security_setting.Basic.name') }
+        </h2>
+
+        {this.state.retrieveError != null && (
+        <div className="alert alert-danger">
+          <p>{t('Error occurred')} : {this.state.err}</p>
+        </div>
+        )}
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.Basic.name')}</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isBasicEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isBasicEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
+              />
+              <label htmlFor="isBasicEnabled">
+                { t('security_setting.Basic.enable_basic') }
+              </label>
+            </div>
+            <p className="help-block">
+              <small>
+                <span dangerouslySetInnerHTML={{ __html: t('security_setting.Basic.desc_1') }} /><br />
+                { t('security_setting.Basic.desc_2')}
+              </small>
+            </p>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
+            && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+        {isBasicEnabled && (
+        <React.Fragment>
+          <div className="row mb-5">
+            <div className="col-xs-offset-3 col-xs-6 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  id="bindByEmail-basic"
+                  type="checkbox"
+                  checked={adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
+                  onChange={() => { adminBasicSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                />
+                <label
+                  htmlFor="bindByEmail-basic"
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical', 'username') }}
+                />
+              </div>
+              <p className="help-block">
+                <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn', 'username') }} />
+              </p>
+            </div>
+          </div>
+
+          <div className="row my-3">
+            <div className="col-xs-offset-4 col-xs-5">
+              <button type="button" className="btn btn-primary" disabled={adminBasicSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
+                {t('Update')}
+              </button>
+            </div>
+          </div>
+
+        </React.Fragment>
+        )}
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+BasicSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
+};
+
+const OidcSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(BasicSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer, AdminBasicSecurityContainer]);
+};
+
+export default withTranslation()(OidcSecurityManagementWrapper);

+ 41 - 0
src/client/js/components/Admin/Security/FacebookSecuritySetting.jsx

@@ -0,0 +1,41 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+
+class FacebookSecurityManagement extends React.Component {
+
+  render() {
+    const { t } = this.props;
+    return (
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          Facebook OAuth { t('security_setting.configuration') }
+        </h2>
+
+        <p className="well">(TBD)</p>
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+
+FacebookSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+};
+
+const TwitterSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(FacebookSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer]);
+};
+
+export default withTranslation()(TwitterSecurityManagementWrapper);

+ 218 - 0
src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -0,0 +1,218 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminGitHubSecurityContainer from '../../../services/AdminGitHubSecurityContainer';
+
+class GitHubSecurityManagement extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+    };
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminGitHubSecurityContainer } = this.props;
+
+    try {
+      await adminGitHubSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+  async onClickSubmit() {
+    const { t, adminGitHubSecurityContainer, adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminGitHubSecurityContainer.updateGitHubSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.OAuth.GitHub.updated_github'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminGitHubSecurityContainer } = this.props;
+    const { isGitHubEnabled } = adminGeneralSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+    return (
+
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          {t('security_setting.OAuth.GitHub.name')}
+        </h2>
+
+        {this.state.retrieveError != null && (
+          <div className="alert alert-danger">
+            <p>{t('Error occurred')} : {this.state.err}</p>
+          </div>
+        )}
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.GitHub.name')}</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isGitHubEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isGitHubEnabled || false}
+                onChange={() => { adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled() }}
+              />
+              <label htmlFor="isGitHubEnabled">
+                {t('security_setting.OAuth.GitHub.enable_github')}
+              </label>
+            </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+        <div className="row mb-5">
+          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
+          <div className="col-xs-6">
+            <input
+              className="form-control"
+              type="text"
+              value={adminGitHubSecurityContainer.state.appSiteUrl}
+              readOnly
+            />
+            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+              <div className="alert alert-danger">
+                <i
+                  className="icon-exclamation"
+                  // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                />
+              </div>
+            )}
+          </div>
+        </div>
+
+
+        {isGitHubEnabled && (
+          <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
+            <div className="row mb-5">
+              <label htmlFor="githubClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="githubClientId"
+                  value={adminGitHubSecurityContainer.state.githubClientId || ''}
+                  onChange={e => adminGitHubSecurityContainer.changeGitHubClientId(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_ID' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="githubClientSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="githubClientSecret"
+                  defaultValue={adminGitHubSecurityContainer.state.githubClientSecret || ''}
+                  onChange={e => adminGitHubSecurityContainer.changeGitHubClientSecret(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_SECRET' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByUserNameGitHub"
+                    type="checkbox"
+                    checked={adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
+                    onChange={() => { adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByUserNameGitHub"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <div className="btn btn-primary" disabled={adminGitHubSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
+                  {t('Update')}
+                </div>
+              </div>
+            </div>
+
+          </React.Fragment>
+        )}
+
+        <hr />
+
+        <div style={{ minHeight: '300px' }}>
+          <h4>
+            <i className="icon-question" aria-hidden="true"></i>
+            <a href="#collapseHelpForGitHubOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.github')}</a>
+          </h4>
+          <ol id="collapseHelpForGitHubOauth" className="collapse">
+            {/* eslint-disable-next-line max-len */}
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_2', { url: adminGitHubSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_3') }} />
+          </ol>
+        </div>
+
+      </React.Fragment>
+
+
+    );
+  }
+
+}
+
+
+GitHubSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
+};
+
+const GitHubSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(GitHubSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer, AdminGitHubSecurityContainer]);
+};
+
+export default withTranslation()(GitHubSecurityManagementWrapper);

+ 225 - 0
src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -0,0 +1,225 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminGoogleSecurityContainer from '../../../services/AdminGoogleSecurityContainer';
+
+class GoogleSecurityManagement extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+    };
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminGoogleSecurityContainer } = this.props;
+
+    try {
+      await adminGoogleSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+  async onClickSubmit() {
+    const { t, adminGoogleSecurityContainer, adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminGoogleSecurityContainer.updateGoogleSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.OAuth.Google.updated_google'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminGoogleSecurityContainer } = this.props;
+    const { isGoogleEnabled } = adminGeneralSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+    return (
+
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          {t('security_setting.OAuth.Google.name')}
+        </h2>
+
+        {this.state.retrieveError != null && (
+          <div className="alert alert-danger">
+            <p>{t('Error occurred')} : {this.state.err}</p>
+          </div>
+        )}
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.Google.name')}</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isGoogleEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isGoogleEnabled || false}
+                onChange={() => { adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled() }}
+              />
+              <label htmlFor="isGoogleEnabled">
+                {t('security_setting.OAuth.Google.enable_google')}
+              </label>
+            </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+        <div className="row mb-5">
+          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
+          <div className="col-xs-6">
+            <input
+              className="form-control"
+              type="text"
+              value={adminGoogleSecurityContainer.state.callbackUrl}
+              readOnly
+            />
+            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+              <div className="alert alert-danger">
+                <i
+                  className="icon-exclamation"
+                  // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                />
+              </div>
+            )}
+          </div>
+        </div>
+
+
+        {isGoogleEnabled && (
+          <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
+            <div className="row mb-5">
+              <label htmlFor="googleClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="googleClientId"
+                  defaultValue={adminGoogleSecurityContainer.state.googleClientId || ''}
+                  onChange={e => adminGoogleSecurityContainer.changeGoogleClientId(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_ID' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="googleClientSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="googleClientSecret"
+                  defaultValue={adminGoogleSecurityContainer.state.googleClientSecret || ''}
+                  onChange={e => adminGoogleSecurityContainer.changeGoogleClientSecret(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_SECRET' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByUserNameGoogle"
+                    type="checkbox"
+                    checked={adminGoogleSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
+                    onChange={() => { adminGoogleSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByUserNameGoogle"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminGoogleSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+
+          </React.Fragment>
+        )}
+
+        <hr />
+
+        <div style={{ minHeight: '300px' }}>
+          <h4>
+            <i className="icon-question" aria-hidden="true"></i>
+            <a href="#collapseHelpForGoogleOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.google')}</a>
+          </h4>
+          <ol id="collapseHelpForGoogleOauth" className="collapse">
+            {/* eslint-disable-next-line max-len */}
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_2') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_3') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_4', { url: adminGoogleSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_5') }} />
+          </ol>
+        </div>
+
+      </React.Fragment>
+
+
+    );
+  }
+
+}
+
+
+GoogleSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
+};
+
+const GoogleSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(GoogleSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer, AdminGoogleSecurityContainer]);
+};
+
+export default withTranslation()(GoogleSecurityManagementWrapper);

+ 171 - 0
src/client/js/components/Admin/Security/LdapAuthTestModal.jsx

@@ -0,0 +1,171 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
+
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+} from 'reactstrap';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminLdapSecurityContainer from '../../../services/AdminLdapSecurityContainer';
+
+const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
+
+class LdapAuthTestModal extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      username: '',
+      password: '',
+      logs: '',
+      errorMessage: null,
+      successMessage: null,
+    };
+
+    this.onChangeUsername = this.onChangeUsername.bind(this);
+    this.onChangePassword = this.onChangePassword.bind(this);
+    this.addLogs = this.addLogs.bind(this);
+    this.testLdapCredentials = this.testLdapCredentials.bind(this);
+  }
+
+  /**
+   * Change username
+   */
+  onChangeUsername(username) {
+    this.setState({ username });
+  }
+
+  /**
+   * Change password
+   */
+  onChangePassword(password) {
+    this.setState({ password });
+  }
+
+  /**
+   * add logs
+   */
+  addLogs(log) {
+    const newLog = `${new Date()} - ${log}\n\n`;
+    this.setState({
+      logs: `${newLog}${this.state.logs}`,
+    });
+  }
+
+  /**
+   * Test ldap auth
+   */
+  async testLdapCredentials() {
+    try {
+      const response = await this.props.appContainer.apiPost('/login/testLdap', {
+        loginForm: {
+          username: this.state.username,
+          password: this.state.password,
+        },
+      });
+
+      // add logs
+      if (response.err) {
+        toastError(response.err);
+        this.addLogs(response.err);
+      }
+
+      if (response.status === 'warning') {
+        this.addLogs(response.message);
+        this.setState({ errorMessage: response.message, successMessage: null });
+      }
+
+      if (response.status === 'success') {
+        toastSuccess(response.message);
+        this.setState({ successMessage: response.message, errorMessage: null });
+      }
+
+      if (response.ldapConfiguration) {
+        const prettified = JSON.stringify(response.ldapConfiguration.server, undefined, 4);
+        this.addLogs(`LDAP Configuration : ${prettified}`);
+      }
+      if (response.ldapAccountInfo) {
+        const prettified = JSON.stringify(response.ldapAccountInfo, undefined, 4);
+        this.addLogs(`Retrieved LDAP Account : ${prettified}`);
+      }
+
+    }
+    // Catch server communication error
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+        <ModalHeader className="modal-header" toggle={this.props.onClose}>
+          Test LDAP Account
+        </ModalHeader>
+        <ModalBody>
+          {this.state.successMessage != null && <div className="alert alert-success">{this.state.successMessage}</div>}
+          {this.state.errorMessage != null && <div className="alert alert-warning">{this.state.errorMessage}</div>}
+          <div className="row p-3">
+            <label htmlFor="username" className="col-xs-3 text-right">{t('username')}</label>
+            <div className="col-xs-6">
+              <input
+                className="form-control"
+                name="username"
+                value={this.state.username}
+                onChange={(e) => { this.onChangeUsername(e.target.value) }}
+              />
+            </div>
+          </div>
+          <div className="row p-3">
+            <label htmlFor="password" className="col-xs-3 text-right">{t('Password')}</label>
+            <div className="col-xs-6">
+              <input
+                className="form-control"
+                type="password"
+                name="password"
+                value={this.state.password}
+                onChange={(e) => { this.onChangePassword(e.target.value) }}
+              />
+            </div>
+          </div>
+          <div>
+            <h5>Logs</h5>
+            <textarea id="taLogs" className="col-xs-12" rows="4" value={this.state.logs} readOnly />
+          </div>
+        </ModalBody>
+        <ModalFooter>
+          <button type="button" className="btn btn-default mt-3 col-xs-offset-5 col-xs-2" onClick={this.testLdapCredentials}>Test</button>
+        </ModalFooter>
+      </Modal>
+    );
+  }
+
+}
+
+
+LdapAuthTestModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+};
+
+const LdapAuthTestModalWrapper = (props) => {
+  return createSubscribedElement(LdapAuthTestModal, props, [AppContainer, AdminLdapSecurityContainer]);
+};
+
+export default withTranslation()(LdapAuthTestModalWrapper);

+ 432 - 0
src/client/js/components/Admin/Security/LdapSecuritySetting.jsx

@@ -0,0 +1,432 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminLdapSecurityContainer from '../../../services/AdminLdapSecurityContainer';
+import LdapAuthTestModal from './LdapAuthTestModal';
+
+
+class LdapSecuritySetting extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+      isLdapAuthTestModalShown: false,
+    };
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+    this.openLdapAuthTestModal = this.openLdapAuthTestModal.bind(this);
+    this.closeLdapAuthTestModal = this.closeLdapAuthTestModal.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminLdapSecurityContainer } = this.props;
+
+    try {
+      await adminLdapSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+  async onClickSubmit() {
+    const { t, adminLdapSecurityContainer, adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminLdapSecurityContainer.updateLdapSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.ldap.updated_ldap'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  openLdapAuthTestModal() {
+    this.setState({ isLdapAuthTestModalShown: true });
+  }
+
+  closeLdapAuthTestModal() {
+    this.setState({ isLdapAuthTestModalShown: false });
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminLdapSecurityContainer } = this.props;
+    const { isLdapEnabled } = adminGeneralSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+    return (
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          LDAP
+        </h2>
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>Use LDAP</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isLdapEnabled"
+                type="checkbox"
+                checked={isLdapEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
+              />
+              <label htmlFor="isLdapEnabled">
+                {t('security_setting.ldap.enable_ldap')}
+              </label>
+            </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+
+        {isLdapEnabled && (
+          <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
+            <div className="row mb-5">
+              <label htmlFor="serverUrl" className="col-xs-3 control-label text-right">Server URL</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="serverUrl"
+                  defaultValue={adminLdapSecurityContainer.state.serverUrl || ''}
+                  onChange={e => adminLdapSecurityContainer.changeServerUrl(e.target.value)}
+                />
+                <small>
+                  <p
+                    className="help-block"
+                    // eslint-disable-next-line react/no-danger
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.server_url_detail') }}
+                  />
+                  {t('security_setting.example')}: <code>ldaps://ldap.company.com/ou=people,dc=company,dc=com</code>
+                </small>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">{t('security_setting.ldap.bind_mode')}</strong>
+              <div className="col-xs-6 text-left">
+                <div className="my-0 btn-group">
+                  <div className="dropdown">
+                    <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                      {adminLdapSecurityContainer.state.isUserBind
+                        ? <span className="pull-left">{t('security_setting.ldap.bind_user')}</span>
+                        : <span className="pull-left">{t('security_setting.ldap.bind_manager')}</span>}
+                      <span className="bs-caret pull-right">
+                        <span className="caret" />
+                      </span>
+                    </button>
+                    {/* TODO adjust dropdown after BS4 */}
+                    <ul className="dropdown-menu" role="menu">
+                      <li key="user" role="presentation" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
+                        <a role="menuitem">{t('security_setting.ldap.bind_user')}</a>
+                      </li>
+                      <li key="manager" role="presentation" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
+                        <a role="menuitem">{t('security_setting.ldap.bind_manager')}</a>
+                      </li>
+                    </ul>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">Bind DN</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="bindDN"
+                  defaultValue={adminLdapSecurityContainer.state.ldapBindDN || ''}
+                  onChange={e => adminLdapSecurityContainer.changeBindDN(e.target.value)}
+                />
+                {(adminLdapSecurityContainer.state.isUserBind === true) ? (
+                  <p className="help-block passport-ldap-userbind">
+                    <small>
+                      {t('security_setting.ldap.bind_DN_user_detail1')}<br />
+                      {/* eslint-disable-next-line react/no-danger */}
+                      <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.bind_DN_user_detail2') }} /><br />
+                      {t('security_setting.example')}1: <code>uid={'{{ username }}'},dc=domain,dc=com</code><br />
+                      {t('security_setting.example')}2: <code>{'{{ username }}'}@domain.com</code>
+                    </small>
+                  </p>
+                )
+                  : (
+                    <p className="help-block passport-ldap-managerbind">
+                      <small>
+                        {t('security_setting.ldap.bind_DN_manager_detail')}<br />
+                        {t('security_setting.example')}1: <code>uid=admin,dc=domain,dc=com</code><br />
+                        {t('security_setting.example')}2: <code>admin@domain.com</code>
+                      </small>
+                    </p>
+                  )}
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="bindDNPassword" className="col-xs-3 text-right">{t('security_setting.ldap.bind_DN_password')}</label>
+              <div className="col-xs-6">
+                {(adminLdapSecurityContainer.state.isUserBind) ? (
+                  <p className="help-block passport-ldap-userbind">
+                    <small>
+                      {t('security_setting.ldap.bind_DN_password_user_detail')}
+                    </small>
+                  </p>
+                )
+                  : (
+                    <>
+                      <p className="help-block passport-ldap-managerbind">
+                        <small>
+                          {t('security_setting.ldap.bind_DN_password_manager_detail')}
+                        </small>
+                      </p>
+                      <input
+                        className="form-control passport-ldap-managerbind"
+                        type="password"
+                        name="bindDNPassword"
+                        defaultValue={adminLdapSecurityContainer.state.ldapBindDNPassword || ''}
+                        onChange={e => adminLdapSecurityContainer.changeBindDNPassword(e.target.value)}
+                      />
+                    </>
+                  )}
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">{t('security_setting.ldap.search_filter')}</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="searchFilter"
+                  defaultValue={adminLdapSecurityContainer.state.ldapSearchFilter || ''}
+                  onChange={e => adminLdapSecurityContainer.changeSearchFilter(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    {t('security_setting.ldap.search_filter_detail1')}<br />
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail2') }} /><br />
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail3') }} />
+                  </small>
+                </p>
+                <p className="help-block">
+                  <small>
+                    {t('security_setting.example')}1 - {t('security_setting.ldap.search_filter_example1')}:
+                    <code>(|(uid={'{{ username }}'})(mail={'{{ username }}'}))</code><br />
+                    {t('security_setting.example')}2 - {t('security_setting.ldap.search_filter_example2')}:
+                    <code>(sAMAccountName={'{{ username }}'})</code>
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <h3 className="alert-anchor border-bottom">
+              Attribute Mapping ({t('security_setting.optional')})
+            </h3>
+
+            <div className="row mb-5">
+              <strong htmlFor="attrMapUsername" className="col-xs-3 text-right">{t('username')}</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  placeholder="Default: uid"
+                  name="attrMapUsername"
+                  defaultValue={adminLdapSecurityContainer.state.ldapAttrMapUsername || ''}
+                  onChange={e => adminLdapSecurityContainer.changeAttrMapUsername(e.target.value)}
+                />
+                <p className="help-block">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.username_detail') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="isSameUsernameTreatedAsIdenticalUser"
+                    type="checkbox"
+                    checked={adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
+                    onChange={() => { adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="isSameUsernameTreatedAsIdenticalUser"
+                    // eslint-disable-next-line react/no-danger
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong htmlFor="attrMapMail" className="col-xs-3 text-right">{t('Email')}</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  placeholder="Default: mail"
+                  name="attrMapMail"
+                  defaultValue={adminLdapSecurityContainer.state.ldapAttrMapMail || ''}
+                  onChange={e => adminLdapSecurityContainer.changeAttrMapMail(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    {t('security_setting.ldap.mail_detail')}
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong htmlFor="attrMapName" className="col-xs-3 text-right">{t('Name')}</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="attrMapName"
+                  defaultValue={adminLdapSecurityContainer.state.ldapAttrMapName || ''}
+                  onChange={e => adminLdapSecurityContainer.changeAttrMapName(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    {t('security_setting.ldap.name_detail')}
+                  </small>
+                </p>
+              </div>
+            </div>
+
+
+            <h3 className="alert-anchor border-bottom">
+              {t('security_setting.ldap.group_search_filter')} ({t('security_setting.optional')})
+            </h3>
+
+            <div className="row mb-5">
+              <strong htmlFor="groupSearchBase" className="col-xs-3 text-right">{t('security_setting.ldap.group_search_base_DN')}</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="groupSearchBase"
+                  defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchBase || ''}
+                  onChange={e => adminLdapSecurityContainer.changeGroupSearchBase(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_base_DN_detail') }} /><br />
+                    {t('security_setting.example')}: <code>ou=groups,dc=domain,dc=com</code>
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong htmlFor="groupSearchFilter" className="col-xs-3 text-right">{t('security_setting.ldap.group_search_filter')}</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="groupSearchFilter"
+                  defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchFilter || ''}
+                  onChange={e => adminLdapSecurityContainer.changeGroupSearchFilter(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    {/* eslint-disable react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail1') }} /><br />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail2') }} /><br />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail3') }} />
+                    {/* eslint-enable react/no-danger */}
+                  </small>
+                </p>
+                <p className="help-block">
+                  <small>
+                    {t('security_setting.example')}:
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail4') }} />
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="groupDnProperty" className="col-xs-3 text-right">{t('security_setting.ldap.group_search_user_DN_property')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  placeholder="Default: uid"
+                  name="groupDnProperty"
+                  defaultValue={adminLdapSecurityContainer.state.ldapGroupDnProperty || ''}
+                  onChange={e => adminLdapSecurityContainer.changeGroupDnProperty(e.target.value)}
+                />
+                <p className="help-block">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_user_DN_property_detail') }} />
+                </p>
+              </div>
+            </div>
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminLdapSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+                <button type="button" className="btn btn-default ml-2" onClick={this.openLdapAuthTestModal}>{t('security_setting.ldap.test_config')}</button>
+              </div>
+            </div>
+
+          </React.Fragment>
+        )}
+
+
+        <LdapAuthTestModal isOpen={this.state.isLdapAuthTestModalShown} onClose={this.closeLdapAuthTestModal} />
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+LdapSecuritySetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
+};
+
+const LdapSecuritySettingWrapper = (props) => {
+  return createSubscribedElement(LdapSecuritySetting, props, [AppContainer, AdminGeneralSecurityContainer, AdminLdapSecurityContainer]);
+};
+
+export default withTranslation()(LdapSecuritySettingWrapper);

+ 202 - 0
src/client/js/components/Admin/Security/LocalSecuritySetting.jsx

@@ -0,0 +1,202 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminLocalSecurityContainer from '../../../services/AdminLocalSecurityContainer';
+
+class LocalSecuritySetting extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+    };
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminLocalSecurityContainer } = this.props;
+
+    try {
+      await adminLocalSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+
+  async onClickSubmit() {
+    const { t, adminGeneralSecurityContainer, adminLocalSecurityContainer } = this.props;
+    try {
+      await adminLocalSecurityContainer.updateLocalSecuritySetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.updated_general_security_setting'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminLocalSecurityContainer } = this.props;
+    const { registrationMode } = adminLocalSecurityContainer.state;
+    const { isLocalEnabled } = adminGeneralSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+
+    return (
+      <React.Fragment>
+        {this.state.retrieveError != null && (
+          <div className="alert alert-danger">
+            <p>{t('Error occurred')} : {this.state.err}</p>
+          </div>
+        )}
+        <h2 className="alert-anchor border-bottom">
+          {t('security_setting.Local.name')}
+        </h2>
+
+        {adminGeneralSecurityContainer.state.useOnlyEnvVarsForSomeOptions && (
+          <p
+            className="alert alert-info"
+            // eslint-disable-next-line max-len
+            dangerouslySetInnerHTML={{ __html: t('security_setting.Local.note for the only env option', { env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+          />
+        )}
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.Local.name')}</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isLocalEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isLocalEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsLocalEnabled() }}
+              />
+              <label htmlFor="isLocalEnabled">
+                {t('security_setting.Local.enable_local')}
+              </label>
+            </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && isLocalEnabled)
+            && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+        {isLocalEnabled && (
+          <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">{t('Register limitation')}</strong>
+              <div className="col-xs-9 text-left">
+                <div className="my-0 btn-group">
+                  <div className="dropdown">
+                    <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                      {registrationMode === 'Open' && <span className="pull-left">{t('security_setting.registration_mode.open')}</span>}
+                      {registrationMode === 'Restricted' && <span className="pull-left">{t('security_setting.registration_mode.restricted')}</span>}
+                      {registrationMode === 'Closed' && <span className="pull-left">{t('security_setting.registration_mode.closed')}</span>}
+                      <span className="bs-caret pull-right">
+                        <span className="caret" />
+                      </span>
+                    </button>
+                    {/* TODO adjust dropdown after BS4 */}
+                    <ul className="dropdown-menu" role="menu">
+                      <li
+                        key="Open"
+                        role="presentation"
+                        type="button"
+                        onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Open') }}
+                      >
+                        <a role="menuitem">{t('security_setting.registration_mode.open')}</a>
+                      </li>
+                      <li
+                        key="Restricted"
+                        role="presentation"
+                        type="button"
+                        onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Restricted') }}
+                      >
+                        <a role="menuitem">{t('security_setting.registration_mode.restricted')}</a>
+                      </li>
+                      <li
+                        key="Closed"
+                        role="presentation"
+                        type="button"
+                        onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Closed') }}
+                      >
+                        <a role="menuitem">{t('security_setting.registration_mode.closed')}</a>
+                      </li>
+                    </ul>
+                  </div>
+                  <p className="help-block">
+                    {t('security_setting.Register limitation desc')}
+                  </p>
+                </div>
+              </div>
+            </div>
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('The whitelist of registration permission E-mail address') }} />
+              <div className="col-xs-6">
+                <div>
+                  <textarea
+                    className="form-control"
+                    type="textarea"
+                    name="registrationWhiteList"
+                    defaultValue={adminLocalSecurityContainer.state.registrationWhiteList.join('\n')}
+                    onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
+                  />
+                  <p className="help-block small">{t('security_setting.restrict_emails')}<br />{t('security_setting.for_instance')}
+                    <code>@growi.org</code>{t('security_setting.only_those')}<br />
+                    {t('security_setting.insert_single')}
+                  </p>
+                </div>
+              </div>
+            </div>
+
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminLocalSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+          </React.Fragment>
+        )}
+
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+LocalSecuritySetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
+};
+
+const LocalSecuritySettingWrapper = (props) => {
+  return createSubscribedElement(LocalSecuritySetting, props, [AppContainer, AdminGeneralSecurityContainer, AdminLocalSecurityContainer]);
+};
+
+export default withTranslation()(LocalSecuritySettingWrapper);

+ 352 - 0
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -0,0 +1,352 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminOidcSecurityContainer from '../../../services/AdminOidcSecurityContainer';
+
+class OidcSecurityManagement extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+    };
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminOidcSecurityContainer } = this.props;
+
+    try {
+      await adminOidcSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+  async onClickSubmit() {
+    const { t, adminOidcSecurityContainer, adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminOidcSecurityContainer.updateOidcSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.OAuth.OIDC.updated_oidc'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminOidcSecurityContainer } = this.props;
+    const { isOidcEnabled } = adminGeneralSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+
+    return (
+
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          {t('security_setting.OAuth.OIDC.name')}
+        </h2>
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.OIDC.name')}</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isOidcEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isOidcEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
+              />
+              <label htmlFor="isOidcEnabled">
+                {t('security_setting.OAuth.enable_oidc')}
+              </label>
+            </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+        <div className="row mb-5">
+          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
+          <div className="col-xs-6">
+            <input
+              className="form-control"
+              type="text"
+              value={adminOidcSecurityContainer.state.callbackUrl}
+              readOnly
+            />
+            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+              <div className="alert alert-danger">
+                <i
+                  className="icon-exclamation"
+                  // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                />
+              </div>
+            )}
+          </div>
+        </div>
+
+        {isOidcEnabled && (
+          <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcProviderName" className="col-xs-3 text-right">{t('security_setting.providerName')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcProviderName"
+                  defaultValue={adminOidcSecurityContainer.state.oidcProviderName || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcProviderName(e.target.value)}
+                />
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcIssuerHost" className="col-xs-3 text-right">{t('security_setting.issuerHost')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcIssuerHost"
+                  defaultValue={adminOidcSecurityContainer.state.oidcIssuerHost || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcIssuerHost(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcClientId"
+                  defaultValue={adminOidcSecurityContainer.state.oidcClientId || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcClientId(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcClientSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcClientSecret"
+                  defaultValue={adminOidcSecurityContainer.state.oidcClientSecret || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcClientSecret(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <h3 className="alert-anchor border-bottom">
+              Attribute Mapping ({t('security_setting.optional')})
+            </h3>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcAttrMapId" className="col-xs-3 text-right">Identifier</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcAttrMapId"
+                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapId || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcAttrMapId(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.id_detail') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcAttrMapUserName" className="col-xs-3 text-right">{t('username')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcAttrMapUserName"
+                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapUserName || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcAttrMapUserName(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.username_detail') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcAttrMapName" className="col-xs-3 text-right">{t('Name')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcAttrMapName"
+                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapName || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcAttrMapName(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.name_detail') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="oidcAttrMapEmail" className="col-xs-3 text-right">{t('Email')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcAttrMapEmail"
+                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapEmail || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcAttrMapEmail(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  defaultValue={adminOidcSecurityContainer.state.callbackUrl || ''}
+                  readOnly
+                />
+                <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+                {!adminGeneralSecurityContainer.state.appSiteUrl && (
+                  <div className="alert alert-danger">
+                    <i
+                      className="icon-exclamation"
+                      // eslint-disable-next-line max-len
+                      dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                    />
+                  </div>
+                )}
+              </div>
+            </div>
+
+            <div className="row mb-3">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByUserName-oidc"
+                    type="checkbox"
+                    checked={adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
+                    onChange={() => { adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByUserName-oidc"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByEmail-oidc"
+                    type="checkbox"
+                    checked={adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
+                    onChange={() => { adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByEmail-oidc"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminOidcSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+          </React.Fragment>
+        )}
+
+
+        <hr />
+
+        <div style={{ minHeight: '300px' }}>
+          <h4>
+            <i className="icon-question" aria-hidden="true" />
+            <a href="#collapseHelpForOidcOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.oidc')}</a>
+          </h4>
+          <ol id="collapseHelpForOidcOauth" className="collapse">
+            <li>{t('security_setting.OAuth.OIDC.register_1')}</li>
+            <li>{t('security_setting.OAuth.OIDC.register_2')}</li>
+            <li>{t('security_setting.OAuth.OIDC.register_3')}</li>
+          </ol>
+        </div>
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+OidcSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
+};
+
+const OidcSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(OidcSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer, AdminOidcSecurityContainer]);
+};
+
+export default withTranslation()(OidcSecurityManagementWrapper);

+ 550 - 0
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -0,0 +1,550 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminSamlSecurityContainer from '../../../services/AdminSamlSecurityContainer';
+
+class SamlSecurityManagement extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+      envEntryPoint: '',
+      envIssuer: '',
+      envCert: '',
+      envAttrMapId: '',
+      envAttrMapUsername: '',
+      envAttrMapMail: '',
+      envAttrMapFirstName: '',
+      envAttrMapLastName: '',
+      envABLCRule: '',
+    };
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminSamlSecurityContainer } = this.props;
+
+    try {
+      const samlAuth = await adminSamlSecurityContainer.retrieveSecurityData();
+      this.setState({
+        envEntryPoint: samlAuth.samlEnvVarEntryPoint,
+        envIssuer: samlAuth.samlEnvVarIssuer,
+        envCert: samlAuth.samlEnvVarCert,
+        envAttrMapId: samlAuth.samlEnvVarAttrMapId,
+        envAttrMapUsername: samlAuth.samlEnvVarAttrMapUsername,
+        envAttrMapMail: samlAuth.samlEnvVarAttrMapMail,
+        envAttrMapFirstName: samlAuth.samlEnvVarAttrMapFirstName,
+        envAttrMapLastName: samlAuth.samlEnvVarAttrMapLastName,
+        envABLCRule: samlAuth.samlEnvVarABLCRule,
+      });
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+  async onClickSubmit() {
+    const { t, adminSamlSecurityContainer, adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminSamlSecurityContainer.updateSamlSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.SAML.updated_saml'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminSamlSecurityContainer } = this.props;
+    const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
+    const { isSamlEnabled } = adminGeneralSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+    return (
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          {t('security_setting.SAML.name')}
+        </h2>
+
+        {useOnlyEnvVars && (
+          <p
+            className="alert alert-info"
+            dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.note for the only env option', { env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+          />
+        )}
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.SAML.name')}</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isSamlEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isSamlEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
+              />
+              <label htmlFor="isSamlEnabled">
+                {t('security_setting.SAML.enable_saml')}
+              </label>
+            </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isSamlEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+        <div className="row mb-5">
+          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
+          <div className="col-xs-6">
+            <input
+              className="form-control"
+              type="text"
+              defaultValue={adminSamlSecurityContainer.state.callbackUrl}
+              readOnly
+            />
+            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
+            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+              <div className="alert alert-danger">
+                <i
+                  className="icon-exclamation"
+                  // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                />
+              </div>
+            )}
+          </div>
+        </div>
+
+        {isSamlEnabled && (
+          <React.Fragment>
+
+            {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
+              <div className="alert alert-danger">
+                {t('security_setting.missing mandatory configs')}
+                <ul>
+                  {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
+                    const key = configKey.replace('security:passport-saml:', '');
+                    return <li key={configKey}>{t(`security_setting.form_item_name.${key}`)}</li>;
+                  })}
+                </ul>
+              </div>
+            )}
+
+
+            <h3 className="alert-anchor border-bottom">
+              Basic Settings
+            </h3>
+
+            <table className={`table settings-table ${adminSamlSecurityContainer.state.useOnlyEnvVars && 'use-only-env-vars'}`}>
+              <colgroup>
+                <col className="item-name" />
+                <col className="from-db" />
+                <col className="from-env-vars" />
+              </colgroup>
+              <thead>
+                <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <th>{t('security_setting.form_item_name.entryPoint')}</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      name="samlEntryPoint"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlEntryPoint}
+                      onChange={e => adminSamlSecurityContainer.changeSamlEntryPoint(e.target.value)}
+                    />
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envEntryPoint || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('security_setting.form_item_name.issuer')}</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      name="samlEnvVarissuer"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlIssuer}
+                      onChange={e => adminSamlSecurityContainer.changeSamlIssuer(e.target.value)}
+                    />
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envIssuer || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('security_setting.form_item_name.cert')}</th>
+                  <td>
+                    <textarea
+                      className="form-control input-sm"
+                      type="text"
+                      rows="5"
+                      name="samlCert"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlCert}
+                      onChange={e => adminSamlSecurityContainer.changeSamlCert(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small>
+                        {t('security_setting.SAML.cert_detail')}
+                      </small>
+                    </p>
+                    <div>
+                      <small>
+                        e.g.
+                        <pre>{`-----BEGIN CERTIFICATE-----
+MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
+UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
+...
+crmVwBzbloUO2l6k1ibwD2WVwpdxMKIF5z58HfKAvxZAzCHE7kMEZr1ge30WRXQA
+pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
+-----END CERTIFICATE-----
+                        `}
+                        </pre>
+                      </small>
+                    </div>
+                  </td>
+                  <td>
+                    <textarea
+                      className="form-control input-sm"
+                      type="text"
+                      rows="5"
+                      readOnly
+                      value={this.state.envCert || ''}
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+
+            <h3 className="alert-anchor border-bottom">
+              Attribute Mapping
+            </h3>
+
+            <table className={`table settings-table ${adminSamlSecurityContainer.state.useOnlyEnvVars && 'use-only-env-vars'}`}>
+              <colgroup>
+                <col className="item-name" />
+                <col className="from-db" />
+                <col className="from-env-vars" />
+              </colgroup>
+              <thead>
+                <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <th>{t('security_setting.form_item_name.attrMapId')}</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapId}
+                      onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small>
+                        {t('security_setting.SAML.id_detail')}
+                      </small>
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envAttrMapId || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('security_setting.form_item_name.attrMapUsername')}</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapUsername}
+                      onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.username_detail') }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envAttrMapUsername || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('security_setting.form_item_name.attrMapMail')}</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapMail}
+                      onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: 'Email' }) }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envAttrMapMail || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('security_setting.form_item_name.attrMapFirstName')}</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapFirstName}
+                      onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
+                    />
+                    <p className="help-block">
+                      {/* eslint-disable-next-line max-len */}
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapFirstName') }) }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envAttrMapFirstName || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small>
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
+                        <br />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.Use default if both are empty', { target: 'firstName' }) }} />
+                      </small>
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('security_setting.form_item_name.attrMapLastName')}</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapLastName}
+                      onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
+                    />
+                    <p className="help-block">
+                      {/* eslint-disable-next-line max-len */}
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapLastName') }) }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envAttrMapLastName || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small>
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
+                        <br />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.Use default if both are empty', { target: 'lastName' }) }} />
+                      </small>
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+
+            <h3 className="alert-anchor border-bottom">
+              Attribute Mapping Options
+            </h3>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByUserName-SAML"
+                    type="checkbox"
+                    checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
+                    onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByUserName-SAML"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByEmail-SAML"
+                    type="checkbox"
+                    checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
+                    onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByEmail-SAML"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <h3 className="alert-anchor border-bottom">
+              Attribute-based Login Control
+            </h3>
+
+            <p className="help-block">
+              <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
+            </p>
+
+            <table className="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
+              <colgroup>
+                <col className="item-name" />
+                <col className="from-db" />
+                <col className="from-env-vars" />
+              </colgroup>
+              <thead>
+                <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <th>
+                    { t('security_setting.form_item_name.ABLCRule') }
+                  </th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      defaultValue={adminSamlSecurityContainer.state.samlABLCRule || ''}
+                      onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
+                      readOnly={useOnlyEnvVars}
+                    />
+                    <p className="help-block">
+                      <small>
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_detail') }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example') }} />
+                      </small>
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={this.state.envABLCRule || ''}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminSamlSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+
+          </React.Fragment>
+        )}
+
+      </React.Fragment>
+    );
+
+  }
+
+}
+
+SamlSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
+};
+
+const SamlSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(SamlSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer, AdminSamlSecurityContainer]);
+};
+
+export default withTranslation()(SamlSecurityManagementWrapper);

+ 124 - 0
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -0,0 +1,124 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import LdapSecuritySetting from './LdapSecuritySetting';
+import LocalSecuritySetting from './LocalSecuritySetting';
+import SamlSecuritySetting from './SamlSecuritySetting';
+import OidcSecuritySetting from './OidcSecuritySetting';
+import SecuritySetting from './SecuritySetting';
+import BasicSecuritySetting from './BasicSecuritySetting';
+import GoogleSecuritySetting from './GoogleSecuritySetting';
+import GitHubSecuritySetting from './GitHubSecuritySetting';
+import TwitterSecuritySetting from './TwitterSecuritySetting';
+import FacebookSecuritySetting from './FacebookSecuritySetting';
+
+class SecurityManagement extends React.Component {
+
+  constructor(props) {
+    super();
+
+  }
+
+  render() {
+    const { t } = this.props;
+    return (
+      <Fragment>
+        <div>
+          <SecuritySetting />
+        </div>
+
+        {/* XSS configuration link */}
+        <div className="mb-5">
+          <h2 className="border-bottom">{t('security_setting.xss_prevent_setting')}</h2>
+          <div className="text-center">
+            <a style={{ fontSize: 'large' }} href="/admin/markdown/#preventXSS">
+              <i className="fa-fw icon-login"></i> {t('security_setting.xss_prevent_setting_link')}
+            </a>
+          </div>
+        </div>
+
+        {/* TODO GW-226 adapt BS4 */}
+        <div className="auth-mechanism-configurations m-t-10">
+          <h2 className="border-bottom">{t('security_setting.Authentication mechanism settings')}</h2>
+          <div className="passport-settings">
+            <ul className="nav nav-tabs" role="tablist">
+              <li className="active">
+                <a href="#passport-local" data-toggle="tab" role="tab"><i className="fa fa-users"></i> ID/Pass</a>
+              </li>
+              <li>
+                <a href="#passport-ldap" data-toggle="tab" role="tab"><i className="fa fa-sitemap"></i> LDAP</a>
+              </li>
+              <li>
+                <a href="#passport-saml" data-toggle="tab" role="tab"><i className="fa fa-key"></i> SAML</a>
+              </li>
+              <li>
+                <a href="#passport-oidc" data-toggle="tab" role="tab"><i className="fa fa-openid"></i> OIDC</a>
+              </li>
+              <li>
+                <a href="#passport-basic" data-toggle="tab" role="tab"><i className="fa fa-lock"></i> Basic</a>
+              </li>
+              <li>
+                <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i className="fa fa-google"></i> Google</a>
+              </li>
+              <li>
+                <a href="#passport-github" data-toggle="tab" role="tab"><i className="fa fa-github"></i> GitHub</a>
+              </li>
+              <li>
+                <a href="#passport-twitter" data-toggle="tab" role="tab"><i className="fa fa-twitter"></i> Twitter</a>
+              </li>
+              <li className="tbd">
+                <a href="#passport-facebook" data-toggle="tab" role="tab"><i className="fa fa-facebook"></i> (TBD) Facebook</a>
+              </li>
+            </ul>
+            <div className="tab-content p-t-10">
+              <div id="passport-local" className="tab-pane active" role="tabpanel">
+                <LocalSecuritySetting />
+              </div>
+              <div id="passport-ldap" className="tab-pane" role="tabpanel">
+                <LdapSecuritySetting />
+              </div>
+              <div id="passport-saml" className="tab-pane" role="tabpanel">
+                <SamlSecuritySetting />
+              </div>
+              <div id="passport-oidc" className="tab-pane" role="tabpanel">
+                <OidcSecuritySetting />
+              </div>
+              <div id="passport-basic" className="tab-pane" role="tabpanel">
+                <BasicSecuritySetting />
+              </div>
+              <div id="passport-google-oauth" className="tab-pane" role="tabpanel">
+                <GoogleSecuritySetting />
+              </div>
+              <div id="passport-github" className="tab-pane" role="tabpanel">
+                <GitHubSecuritySetting />
+              </div>
+              <div id="passport-twitter" className="tab-pane" role="tabpanel">
+                <TwitterSecuritySetting />
+              </div>
+              <div id="passport-facebook" className="tab-pane" role="tabpanel">
+                <FacebookSecuritySetting />
+              </div>
+            </div>
+          </div>
+        </div>
+      </Fragment>
+    );
+  }
+
+}
+
+SecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  csrf: PropTypes.string,
+};
+
+const SecurityManagementWrapper = (props) => {
+  return createSubscribedElement(SecurityManagement, props, [AppContainer]);
+};
+
+export default withTranslation()(SecurityManagementWrapper);

+ 231 - 0
src/client/js/components/Admin/Security/SecuritySetting.jsx

@@ -0,0 +1,231 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+
+class SecuritySetting extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      retrieveError: null,
+    };
+    this.putSecuritySetting = this.putSecuritySetting.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminGeneralSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+      this.setState({ retrieveError: err.message });
+    }
+  }
+
+  async putSecuritySetting() {
+    const { t, adminGeneralSecurityContainer } = this.props;
+    try {
+      await adminGeneralSecurityContainer.updateGeneralSecuritySetting();
+      toastSuccess(t('security_setting.updated_general_security_setting'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer } = this.props;
+    const { currentRestrictGuestMode, currentPageCompleteDeletionAuthority } = adminGeneralSecurityContainer.state;
+    const helpPageListingByOwner = { __html: t('security_setting.page_listing_1') };
+    const helpPageListingByGroup = { __html: t('security_setting.page_listing_2') };
+    // eslint-disable-next-line max-len
+    const helpForceWikiMode = { __html: t('security_setting.Fixed by env var', { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }) };
+
+
+    return (
+      <React.Fragment>
+        <fieldset>
+          <h2 className="alert-anchor border-bottom">
+            {t('security_settings')}
+          </h2>
+          {this.state.retrieveError != null && (
+            <div className="alert alert-danger">
+              <p>{t('Error occurred')} : {this.state.retrieveError}</p>
+            </div>
+          )}
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right"> {t('security_setting.Guest Users Access')} </strong>
+            <div className="col-xs-9 text-left">
+              <div className="my-0 btn-group">
+                <div className="dropdown">
+                  <button
+                    className="btn btn-default dropdown-toggle w-100"
+                    type="button"
+                    data-toggle="dropdown"
+                    aria-haspopup="true"
+                    aria-expanded="false"
+                    disabled={adminGeneralSecurityContainer.state.isWikiModeForced}
+                  >
+                    <span className="pull-left">
+                      {currentRestrictGuestMode === 'Deny' && t('security_setting.guest_mode.deny')}
+                      {currentRestrictGuestMode === 'Readonly' && t('security_setting.guest_mode.readonly')}
+                    </span>
+                    <span className="bs-caret pull-right">
+                      <span className="caret" />
+                    </span>
+                  </button>
+                  {/* TODO adjust dropdown after BS4 */}
+                  <ul className="dropdown-menu" role="menu">
+                    <li
+                      key="Deny"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}
+                    >
+                      <a role="menuitem">{t('security_setting.guest_mode.deny')}</a>
+                    </li>
+                    <li
+                      key="Readonly"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}
+                    >
+                      <a role="menuitem">{t('security_setting.guest_mode.readonly')}</a>
+                    </li>
+                  </ul>
+                </div>
+              </div>
+            </div>
+          </div>
+          {adminGeneralSecurityContainer.state.isWikiModeForced && (
+            <div className="row mb-5">
+              <div className="col-xs-3 text-right" />
+              <div className="col-xs-9 text-left">
+                <p className="alert alert-warning mt-2 text-left">
+                  <i className="icon-exclamation icon-fw">
+                  </i><b>FIXED</b><br />
+                  {<b dangerouslySetInnerHTML={helpForceWikiMode} />}
+                </p>
+              </div>
+            </div>
+          )}
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={helpPageListingByOwner} />
+            <div className="col-xs-6 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  id="isShowRestrictedByOwner"
+                  type="checkbox"
+                  checked={adminGeneralSecurityContainer.state.isShowRestrictedByOwner}
+                  onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByOwner() }}
+                />
+                <label htmlFor="isShowRestrictedByOwner">
+                  {t('security_setting.page_listing_1_desc')}
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={helpPageListingByGroup} />
+            <div className="col-xs-6 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  id="isShowRestrictedByGroup"
+                  type="checkbox"
+                  checked={adminGeneralSecurityContainer.state.isShowRestrictedByGroup}
+                  onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByGroup() }}
+                />
+                <label htmlFor="isShowRestrictedByGroup">
+                  {t('security_setting.page_listing_2_desc')}
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right"> {t('security_setting.complete_deletion')} </strong>
+            <div className="col-xs-9 text-left">
+              <div className="my-0 btn-group">
+                <div className="dropdown">
+                  <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                    <span className="pull-left">
+                      {currentPageCompleteDeletionAuthority === 'anyOne' && t('security_setting.anyone')}
+                      {currentPageCompleteDeletionAuthority === 'adminOnly' && t('security_setting.admin_only')}
+                      {(currentPageCompleteDeletionAuthority === 'adminAndAuthor' || currentPageCompleteDeletionAuthority == null)
+                        && t('security_setting.admin_and_author')}
+                    </span>
+                    <span className="bs-caret pull-right">
+                      <span className="caret" />
+                    </span>
+                  </button>
+                  {/* TODO adjust dropdown after BS4 */}
+                  <ul className="dropdown-menu" role="menu">
+                    <li
+                      key="anyone"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}
+                    >
+                      <a role="menuitem">{t('security_setting.anyone')}</a>
+                    </li>
+                    <li
+                      key="admin_only"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}
+                    >
+                      <a role="menuitem">{t('security_setting.admin_only')}</a>
+                    </li>
+                    <li
+                      key="admin_and_author"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminAndAuthor') }}
+                    >
+                      <a role="menuitem">{t('security_setting.admin_and_author')}</a>
+                    </li>
+                  </ul>
+                </div>
+                <p className="help-block small">
+                  {t('security_setting.complete_deletion_explain')}
+                </p>
+              </div>
+            </div>
+          </div>
+          <div className="row my-3">
+            <div className="col-xs-offset-3 col-xs-5">
+              <button type="submit" className="btn btn-primary" disabled={this.state.retrieveError != null} onClick={this.putSecuritySetting}>
+                {t('Update')}
+              </button>
+            </div>
+          </div>
+        </fieldset>
+      </React.Fragment>
+    );
+  }
+
+}
+
+SecuritySetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  csrf: PropTypes.string,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+};
+
+const SecuritySettingWrapper = (props) => {
+  return createSubscribedElement(SecuritySetting, props, [AppContainer, AdminGeneralSecurityContainer]);
+};
+
+export default withTranslation()(SecuritySettingWrapper);

+ 226 - 0
src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -0,0 +1,226 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminTwitterSecurityContainer from '../../../services/AdminTwitterSecurityContainer';
+
+class TwitterSecurityManagement extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      retrieveError: null,
+    };
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async componentDidMount() {
+    const { adminTwitterSecurityContainer } = this.props;
+
+    try {
+      await adminTwitterSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      toastError(err);
+    }
+    this.setState({ isRetrieving: false });
+  }
+
+  async onClickSubmit() {
+    const { t, adminTwitterSecurityContainer, adminGeneralSecurityContainer } = this.props;
+
+    try {
+      await adminTwitterSecurityContainer.updateTwitterSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      toastSuccess(t('security_setting.OAuth.Twitter.updated_twitter'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminTwitterSecurityContainer } = this.props;
+    const { isTwitterEnabled } = adminTwitterSecurityContainer.state;
+
+    if (this.state.isRetrieving) {
+      return null;
+    }
+    return (
+
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          {t('security_setting.OAuth.Twitter.name')}
+        </h2>
+
+        {this.state.retrieveError != null && (
+          <div className="alert alert-danger">
+            <p>{t('Error occurred')} : {this.state.err}</p>
+          </div>
+        )}
+
+        <div className="row mb-5">
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.Twitter.name')}</strong>
+          </div>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isTwitterEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isTwitterEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsTwitterOAuthEnabled() }}
+              />
+              <label htmlFor="isTwitterEnabled">
+                {t('security_setting.OAuth.Twitter.enable_twitter')}
+              </label>
+            </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('twitter') && isTwitterEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+          </div>
+        </div>
+
+        <div className="row mb-5">
+          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
+          <div className="col-xs-6">
+            <input
+              className="form-control"
+              type="text"
+              value={adminTwitterSecurityContainer.state.callbackUrl}
+              readOnly
+            />
+            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+              <div className="alert alert-danger">
+                <i
+                  className="icon-exclamation"
+                  // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                />
+              </div>
+            )}
+          </div>
+        </div>
+
+
+        {isTwitterEnabled && (
+          <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
+            <div className="row mb-5">
+              <label htmlFor="TwitterConsumerId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="TwitterConsumerId"
+                  defaultValue={adminTwitterSecurityContainer.state.twitterConsumerKey || ''}
+                  onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerKey(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_KEY' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="TwitterConsumerSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="TwitterConsumerSecret"
+                  defaultValue={adminTwitterSecurityContainer.state.twitterConsumerSecret || ''}
+                  onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerSecret(e.target.value)}
+                />
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_SECRET' }) }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByUserNameTwitter"
+                    type="checkbox"
+                    checked={adminTwitterSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
+                    onChange={() => { adminTwitterSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByUserNameTwitter"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminTwitterSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+
+          </React.Fragment>
+        )}
+
+        <hr />
+
+        <div style={{ minHeight: '300px' }}>
+          <h4>
+            <i className="icon-question" aria-hidden="true"></i>
+            <a href="#collapseHelpForTwitterOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.twitter')}</a>
+          </h4>
+          <ol id="collapseHelpForTwitterOauth" className="collapse">
+            {/* eslint-disable-next-line max-len */}
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_1', { link: '<a href="https://apps.twitter.com/" target=_blank>Twitter Application Management</a>' }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_2') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_3') }} />
+            {/* eslint-disable-next-line max-len */}
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_4', { url: adminTwitterSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_5') }} />
+          </ol>
+        </div>
+
+      </React.Fragment>
+
+
+    );
+  }
+
+}
+
+
+TwitterSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
+};
+
+const TwitterSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(TwitterSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer, AdminTwitterSecurityContainer]);
+};
+
+export default withTranslation()(TwitterSecurityManagementWrapper);

+ 79 - 0
src/client/js/components/Drawio.jsx

@@ -0,0 +1,79 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+class Drawio extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.drawioContainer = React.createRef();
+
+    this.style = {
+      borderRadius: 3,
+      border: '1px solid #d7d7d7',
+      margin: '20px 0',
+    };
+
+    this.isPreview = this.props.isPreview;
+    this.drawioContent = this.props.drawioContent;
+
+    this.onEdit = this.onEdit.bind(this);
+  }
+
+  onEdit() {
+    if (window.crowi != null) {
+      window.crowi.launchDrawioModal('page',
+        this.props.rangeLineNumberOfMarkdown.beginLineNumber,
+        this.props.rangeLineNumberOfMarkdown.endLineNumber);
+    }
+  }
+
+  componentDidMount() {
+    const DrawioViewer = window.GraphViewer;
+    if (DrawioViewer != null) {
+      DrawioViewer.processElements();
+    }
+  }
+
+  renderContents() {
+    return this.drawioContent;
+  }
+
+  render() {
+    return (
+      <div className="editable-with-drawio position-relative">
+        { !this.isPreview
+          && (
+          <button type="button" className="drawio-iframe-trigger position-absolute btn" onClick={this.onEdit}>
+            <i className="icon-note mr-1"></i>{this.props.t('Edit')}
+          </button>
+          )
+        }
+        <div
+          className="drawio"
+          style={this.style}
+          ref={(c) => { this.drawioContainer = c }}
+          onScroll={(event) => {
+            event.preventDefault();
+          }}
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: this.renderContents() }}
+        >
+        </div>
+      </div>
+    );
+  }
+
+}
+
+Drawio.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.object.isRequired,
+  drawioContent: PropTypes.any.isRequired,
+  isPreview: PropTypes.bool,
+  rangeLineNumberOfMarkdown: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(Drawio);

+ 48 - 0
src/client/js/components/Page.jsx

@@ -11,7 +11,9 @@ import MarkdownTable from '../models/MarkdownTable';
 
 import RevisionRenderer from './Page/RevisionRenderer';
 import HandsontableModal from './PageEditor/HandsontableModal';
+import DrawioModal from './PageEditor/DrawioModal';
 import mtu from './PageEditor/MarkdownTableUtil';
+import mdu from './PageEditor/MarkdownDrawioUtil';
 
 const logger = loggerFactory('growi:Page');
 
@@ -22,11 +24,13 @@ class Page extends React.Component {
 
     this.state = {
       currentTargetTableArea: null,
+      currentTargetDrawioArea: null,
     };
 
     this.growiRenderer = this.props.appContainer.getRenderer('page');
 
     this.saveHandlerForHandsontableModal = this.saveHandlerForHandsontableModal.bind(this);
+    this.saveHandlerForDrawioModal = this.saveHandlerForDrawioModal.bind(this);
   }
 
   componentWillMount() {
@@ -45,6 +49,19 @@ class Page extends React.Component {
     this.handsontableModal.show(MarkdownTable.fromMarkdownString(tableLines));
   }
 
+  /**
+   * launch DrawioModal with data specified by arguments
+   * @param beginLineNumber
+   * @param endLineNumber
+   */
+  launchDrawioModal(beginLineNumber, endLineNumber) {
+    const markdown = this.props.pageContainer.state.markdown;
+    const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber, endLineNumber);
+    const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
+    this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
+    this.drawioModal.show(drawioData);
+  }
+
   async saveHandlerForHandsontableModal(markdownTable) {
     const { pageContainer, editorContainer } = this.props;
     const optionsToSave = editorContainer.getCurrentOptionsToSave();
@@ -75,6 +92,36 @@ class Page extends React.Component {
     }
   }
 
+  async saveHandlerForDrawioModal(drawioData) {
+    const { pageContainer, editorContainer } = this.props;
+    const optionsToSave = editorContainer.getCurrentOptionsToSave();
+
+    const newMarkdown = mdu.replaceDrawioInMarkdown(
+      drawioData,
+      this.props.pageContainer.state.markdown,
+      this.state.currentTargetDrawioArea.beginLineNumber,
+      this.state.currentTargetDrawioArea.endLineNumber,
+    );
+
+    try {
+      // disable unsaved warning
+      editorContainer.disableUnsavedWarning();
+
+      // eslint-disable-next-line no-unused-vars
+      const { page, tags } = await pageContainer.save(newMarkdown, optionsToSave);
+      logger.debug('success to save');
+
+      pageContainer.showSuccessToastr();
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      pageContainer.showErrorToastr(error);
+    }
+    finally {
+      this.setState({ currentTargetDrawioArea: null });
+    }
+  }
+
   render() {
     const isMobile = this.props.appContainer.isMobile;
     const { markdown } = this.props.pageContainer.state;
@@ -83,6 +130,7 @@ class Page extends React.Component {
       <div className={isMobile ? 'page-mobile' : ''}>
         <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
         <HandsontableModal ref={(c) => { this.handsontableModal = c }} onSave={this.saveHandlerForHandsontableModal} />
+        <DrawioModal ref={(c) => { this.drawioModal = c }} onSave={this.saveHandlerForDrawioModal} />
       </div>
     );
   }

+ 19 - 0
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -17,8 +17,10 @@ import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
 import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
 import MarkdownTableInterceptor from './MarkdownTableInterceptor';
 import mtu from './MarkdownTableUtil';
+import mdu from './MarkdownDrawioUtil';
 import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
+import DrawioModal from './DrawioModal';
 
 const loadScript = require('simple-load-script');
 const loadCssSync = require('load-css-file');
@@ -94,6 +96,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
     this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
     this.showHandsonTableHandler = this.showHandsonTableHandler.bind(this);
+    this.showDrawioHandler = this.showDrawioHandler.bind(this);
   }
 
   init() {
@@ -647,6 +650,10 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.handsontableModal.show(mtu.getMarkdownTable(this.getCodeMirror()));
   }
 
+  showDrawioHandler() {
+    this.drawioIFrame.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
+  }
+
   getNavbarItems() {
     return [
       /* eslint-disable max-len */
@@ -746,6 +753,14 @@ export default class CodeMirrorEditor extends AbstractEditor {
       >
         <EditorIcon icon="Table" />
       </Button>,
+      <Button
+        key="nav-item-drawio"
+        bsSize="small"
+        title="draw.io"
+        onClick={this.showDrawioHandler}
+      >
+        <EditorIcon icon="Drawio" />
+      </Button>,
       /* eslint-able max-len */
     ];
   }
@@ -824,6 +839,10 @@ export default class CodeMirrorEditor extends AbstractEditor {
           ref={(c) => { this.handsontableModal = c }}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
         />
+        <DrawioModal
+          ref={(c) => { this.drawioIFrame = c }}
+          onSave={(drawioData) => { return mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioData) }}
+        />
 
       </React.Fragment>
     );

+ 159 - 0
src/client/js/components/PageEditor/DrawioModal.jsx

@@ -0,0 +1,159 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import i18next from 'i18next';
+
+import {
+  Modal,
+  ModalBody,
+} from 'reactstrap';
+
+export default class DrawioModal extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      show: false,
+      drawioMxFile: '',
+    };
+
+    this.drawioIFrame = React.createRef();
+
+    this.headerColor = '#334455';
+    this.fontFamily = "Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
+
+    this.init = this.init.bind(this);
+    this.cancel = this.cancel.bind(this);
+    this.receiveFromDrawio = this.receiveFromDrawio.bind(this);
+    this.drawioURL = this.drawioURL.bind(this);
+  }
+
+  init(drawioMxFile) {
+    const initDrawioMxFile = drawioMxFile;
+    this.setState(
+      {
+        drawioMxFile: initDrawioMxFile,
+      },
+    );
+  }
+
+  show(drawioMxFile) {
+    this.init(drawioMxFile);
+
+    window.addEventListener('message', this.receiveFromDrawio);
+    this.setState({ show: true });
+  }
+
+  hide() {
+    this.setState({
+      show: false,
+    });
+  }
+
+  cancel() {
+    this.hide();
+  }
+
+  receiveFromDrawio(event) {
+    if (event.data === 'ready') {
+      event.source.postMessage(this.state.drawioMxFile, '*');
+      return;
+    }
+
+    if (event.data === '{"event":"configure"}') {
+      if (event.source == null) {
+        return;
+      }
+
+      // refs:
+      //  * https://desk.draw.io/support/solutions/articles/16000103852-how-to-customise-the-draw-io-interface
+      //  * https://desk.draw.io/support/solutions/articles/16000042544-how-does-embed-mode-work-
+      //  * https://desk.draw.io/support/solutions/articles/16000058316-how-to-configure-draw-io-
+      event.source.postMessage(JSON.stringify({
+        action: 'configure',
+        config: {
+          css: `
+          .geMenubarContainer { background-color: ${this.headerColor} !important; }
+          .geMenubar { background-color: ${this.headerColor} !important; }
+          .geEditor { font-family: ${this.fontFamily} !important; }
+          html td.mxPopupMenuItem {
+            font-family: ${this.fontFamily} !important;
+            font-size: 8pt !important;
+          }
+          `,
+          customFonts: ['Lato', 'Charter'],
+        },
+      }), '*');
+
+      return;
+    }
+
+    if (typeof event.data === 'string' && event.data.match(/mxfile/)) {
+      if (event.data.length > 0) {
+        const parser = new DOMParser();
+        const dom = parser.parseFromString(event.data, 'text/xml');
+        const value = dom.getElementsByTagName('diagram')[0].innerHTML;
+        this.props.onSave(value);
+      }
+
+      window.removeEventListener('message', this.receiveFromDrawio);
+      this.hide();
+
+      return;
+    }
+
+    if (typeof event.data === 'string' && event.data.length === 0) {
+      window.removeEventListener('message', this.receiveFromDrawio);
+      this.hide();
+
+      return;
+    }
+
+    // NOTHING DONE. (Receive unknown iframe message.)
+  }
+
+  drawioURL() {
+    const url = new URL('https://www.draw.io/');
+
+    // refs: https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported-
+    url.searchParams.append('spin', 1);
+    url.searchParams.append('embed', 1);
+    url.searchParams.append('lang', i18next.language);
+    url.searchParams.append('ui', 'atlas');
+    url.searchParams.append('configure', 1);
+
+    return url;
+  }
+
+  render() {
+    return (
+      // <Modal show={this.state.show} onHide={this.cancel} bsSize="large" dialogClassName={dialogClassName} keyboard={false}>
+      <Modal isOpen={this.state.show} toggle={this.cancel} className="drawio-modal" bsSize="large" keyboard={false}>
+        <ModalBody className="p-0">
+          {/* Loading spinner */}
+          <div className="w-100 h-100 position-absolute d-flex">
+            <div className="mx-auto my-auto">
+              <i className="fa fa-3x fa-spinner fa-pulse mx-auto text-muted"></i>
+            </div>
+          </div>
+          {/* iframe */}
+          <div className="w-100 h-100 position-absolute d-flex">
+            { this.state.show && (
+              <iframe
+                ref={(c) => { this.drawioIFrame = c }}
+                src={this.drawioURL()}
+                className="border-0 flex-grow-1"
+              >
+              </iframe>
+            ) }
+          </div>
+        </ModalBody>
+      </Modal>
+    );
+  }
+
+}
+
+DrawioModal.propTypes = {
+  onSave: PropTypes.func,
+};

Разница между файлами не показана из-за своего большого размера
+ 3 - 0
src/client/js/components/PageEditor/EditorIcon.jsx


+ 161 - 0
src/client/js/components/PageEditor/MarkdownDrawioUtil.js

@@ -0,0 +1,161 @@
+/**
+ * Utility for markdown drawio
+ */
+class MarkdownDrawioUtil {
+
+  constructor() {
+    this.lineBeginPartOfDrawioRE = /^:::(\s.*)drawio$/;
+    this.lineEndPartOfDrawioRE = /^:::$/;
+  }
+
+  /**
+   * return the postion of the BOD(beginning of drawio)
+   * (If the cursor is not in a drawio block, return its position)
+   */
+  getBod(editor) {
+    const curPos = editor.getCursor();
+    const firstLine = editor.getDoc().firstLine();
+
+    if (this.lineBeginPartOfDrawioRE.test(editor.getDoc().getLine(curPos.line))) {
+      return { line: curPos.line, ch: 0 };
+    }
+
+    let line = curPos.line - 1;
+    let isFound = false;
+    for (; line >= firstLine; line--) {
+      const strLine = editor.getDoc().getLine(line);
+      if (this.lineBeginPartOfDrawioRE.test(strLine)) {
+        isFound = true;
+        break;
+      }
+
+      if (this.lineEndPartOfDrawioRE.test(strLine)) {
+        isFound = false;
+        break;
+      }
+    }
+
+    if (!isFound) {
+      return { line: curPos.line, ch: curPos.ch };
+    }
+
+    const bodLine = Math.max(firstLine, line);
+    return { line: bodLine, ch: 0 };
+  }
+
+  /**
+   * return the postion of the EOD(end of drawio)
+   * (If the cursor is not in a drawio block, return its position)
+   */
+  getEod(editor) {
+    const curPos = editor.getCursor();
+    const lastLine = editor.getDoc().lastLine();
+
+    if (this.lineEndPartOfDrawioRE.test(editor.getDoc().getLine(curPos.line))) {
+      return { line: curPos.line, ch: editor.getDoc().getLine(curPos.line).length };
+    }
+
+    let line = curPos.line + 1;
+    let isFound = false;
+    for (; line <= lastLine; line++) {
+      const strLine = editor.getDoc().getLine(line);
+      if (this.lineEndPartOfDrawioRE.test(strLine)) {
+        isFound = true;
+        break;
+      }
+
+      if (this.lineBeginPartOfDrawioRE.test(strLine)) {
+        isFound = false;
+        break;
+      }
+    }
+
+    if (!isFound) {
+      return { line: curPos.line, ch: curPos.ch };
+    }
+
+    const eodLine = Math.min(line, lastLine);
+    const lineLength = editor.getDoc().getLine(eodLine).length;
+    return { line: eodLine, ch: lineLength };
+  }
+
+  /**
+   * return boolean value whether the cursor position is in a drawio
+   */
+  isInDrawioBlock(editor) {
+    return (this.getBod(editor) !== this.getEod(editor));
+  }
+
+  /**
+   * return drawioData instance where the cursor is
+   * (If the cursor is not in a drawio block, return current line)
+   */
+  getMarkdownDrawioMxfile(editor) {
+    const curPos = editor.getCursor();
+
+    if (this.isInDrawioBlock(editor)) {
+      const bod = this.getBod(editor);
+      const eod = this.getEod(editor);
+
+      // skip block begin sesion("::: drawio")
+      bod.line++;
+      // skip block end sesion(":::")
+      eod.line--;
+      eod.ch = editor.getDoc().getLine(eod.line).length;
+
+      return editor.getDoc().getRange(bod, eod);
+    }
+
+    return editor.getDoc().getLine(curPos.line);
+  }
+
+  replaceFocusedDrawioWithEditor(editor, drawioData) {
+    const curPos = editor.getCursor();
+    const drawioBlock = ['::: drawio', drawioData.toString(), ':::'].join('\n');
+    let beginPos;
+    let endPos;
+
+    if (this.isInDrawioBlock(editor)) {
+      beginPos = this.getBod(editor);
+      endPos = this.getEod(editor);
+    }
+    else {
+      beginPos = { line: curPos.line, ch: curPos.ch };
+      endPos = { line: curPos.line, ch: curPos.ch };
+    }
+
+    editor.getDoc().replaceRange(drawioBlock, beginPos, endPos);
+  }
+
+  /**
+   * return markdown where the drawioData specified by line number params is replaced to the drawioData specified by drawioData param
+   * @param {string} drawioData
+   * @param {string} markdown
+   * @param beginLineNumber
+   * @param endLineNumber
+   */
+  replaceDrawioInMarkdown(drawioData, markdown, beginLineNumber, endLineNumber) {
+    const splitMarkdown = markdown.split(/\r\n|\r|\n/);
+    const markdownBeforeDrawio = splitMarkdown.slice(0, beginLineNumber);
+    const markdownAfterDrawio = splitMarkdown.slice(endLineNumber);
+
+    let newMarkdown = '';
+    if (markdownBeforeDrawio.length > 0) {
+      newMarkdown += `${markdownBeforeDrawio.join('\n')}\n`;
+      newMarkdown += '::: drawio\n';
+    }
+    newMarkdown += drawioData;
+    if (markdownAfterDrawio.length > 0) {
+      newMarkdown += '\n:::';
+      newMarkdown += `\n${markdownAfterDrawio.join('\n')}`;
+    }
+
+    return newMarkdown;
+  }
+
+}
+
+// singleton pattern
+const instance = new MarkdownDrawioUtil();
+Object.freeze(instance);
+export default instance;

+ 74 - 0
src/client/js/services/AdminBasicSecurityContainer.js

@@ -0,0 +1,74 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
+
+/**
+ * Service container for admin security page (BasicSecuritySetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminBasicSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      isSameUsernameTreatedAsIdenticalUser: false,
+    };
+
+  }
+
+  /**
+   * retrieve security data
+   */
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { basicAuth } = response.data.securityParams;
+      this.setState({
+        isSameUsernameTreatedAsIdenticalUser: basicAuth.isSameUsernameTreatedAsIdenticalUser,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminBasicSecurityContainer';
+  }
+
+  /**
+   * Switch isSameUsernameTreatedAsIdenticalUser
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Update basicSetting
+   */
+  async updateBasicSetting() {
+    let requestParams = { isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/basic', requestParams);
+    const { securitySettingParams } = response.data;
+
+    this.setState({
+      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+    });
+    return response;
+  }
+
+}

+ 215 - 0
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -0,0 +1,215 @@
+import { Container } from 'unstated';
+
+import { toastError } from '../util/apiNotification';
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+/**
+ * Service container for admin security page (SecuritySetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminGeneralSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      isWikiModeForced: false,
+      wikiMode: '',
+      currentRestrictGuestMode: 'Deny',
+      currentPageCompleteDeletionAuthority: 'adminOnly',
+      isShowRestrictedByOwner: false,
+      isShowRestrictedByGroup: false,
+      useOnlyEnvVarsForSomeOptions: false,
+      appSiteUrl: appContainer.config.crowi.url || '',
+      isLocalEnabled: false,
+      isLdapEnabled: false,
+      isSamlEnabled: false,
+      isOidcEnabled: false,
+      isBasicEnabled: false,
+      isGoogleEnabled: false,
+      isGitHubEnabled: false,
+      isTwitterEnabled: false,
+      setupStrategies: [],
+    };
+
+    this.onIsWikiModeForced = this.onIsWikiModeForced.bind(this);
+  }
+
+  async retrieveSecurityData() {
+    await this.retrieveSetupStratedies();
+    const response = await this.appContainer.apiv3.get('/security-setting/');
+    const { generalSetting, generalAuth } = response.data.securityParams;
+    this.onIsWikiModeForced(generalSetting.wikiMode);
+    this.setState({
+      currentPageCompleteDeletionAuthority: generalSetting.pageCompleteDeletionAuthority,
+      isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
+      isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
+      wikiMode: generalSetting.wikiMode,
+      isLocalEnabled: generalAuth.isLocalEnabled,
+      isLdapEnabled: generalAuth.isLdapEnabled,
+      isSamlEnabled: generalAuth.isSamlEnabled,
+      isOidcEnabled: generalAuth.isOidcEnabled,
+      isBasicEnabled: generalAuth.isBasicEnabled,
+      isGoogleEnabled: generalAuth.isGoogleEnabled,
+      isGitHubEnabled: generalAuth.isGitHubEnabled,
+      isTwitterEnabled: generalAuth.isTwitterEnabled,
+    });
+  }
+
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminGeneralSecurityContainer';
+  }
+
+  /**
+   * Change restrictGuestMode
+   */
+  changeRestrictGuestMode(restrictGuestModeLabel) {
+    this.setState({ currentRestrictGuestMode: restrictGuestModeLabel });
+  }
+
+  /**
+   * Change pageCompleteDeletionAuthority
+   */
+  changePageCompleteDeletionAuthority(pageCompleteDeletionAuthorityLabel) {
+    this.setState({ currentPageCompleteDeletionAuthority: pageCompleteDeletionAuthorityLabel });
+  }
+
+  /**
+   * Switch showRestrictedByOwner
+   */
+  switchIsShowRestrictedByOwner() {
+    this.setState({ isShowRestrictedByOwner:  !this.state.isShowRestrictedByOwner });
+  }
+
+  /**
+   * Switch showRestrictedByGroup
+   */
+  switchIsShowRestrictedByGroup() {
+    this.setState({ isShowRestrictedByGroup:  !this.state.isShowRestrictedByGroup });
+  }
+
+  onIsWikiModeForced(wikiModeSetting) {
+    if (wikiModeSetting === 'private') {
+      this.setState({ isWikiModeForced: true });
+    }
+    else {
+      this.setState({ isWikiModeForced: false });
+    }
+  }
+
+
+  /**
+   * Update restrictGuestMode
+   * @memberOf AdminGeneralSecuritySContainer
+   * @return {string} Appearance
+   */
+  async updateGeneralSecuritySetting() {
+
+    let requestParams = {
+      restrictGuestMode: this.state.currentRestrictGuestMode,
+      pageCompleteDeletionAuthority: this.state.currentPageCompleteDeletionAuthority,
+      hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
+      hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
+    };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/general-setting', requestParams);
+    const { securitySettingParams } = response.data;
+    return securitySettingParams;
+  }
+
+  /**
+   * Switch authentication
+   */
+  async switchAuthentication(stateVariableName, authId) {
+    const isEnabled = !this.state[stateVariableName];
+    try {
+      await this.appContainer.apiv3.put('/security-setting/authentication/enabled', {
+        isEnabled,
+        authId,
+      });
+      await this.retrieveSetupStratedies();
+      this.setState({ [stateVariableName]: isEnabled });
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  /**
+   * Retrieve SetupStratedies
+   */
+  async retrieveSetupStratedies() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/authentication');
+      const { setupStrategies } = response.data;
+      this.setState({ setupStrategies });
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  /**
+   * Switch local enabled
+   */
+  async switchIsLocalEnabled() {
+    this.switchAuthentication('isLocalEnabled', 'local');
+  }
+
+  /**
+   * Switch LDAP enabled
+   */
+  async switchIsLdapEnabled() {
+    this.switchAuthentication('isLdapEnabled', 'ldap');
+  }
+
+  /**
+   * Switch SAML enabled
+   */
+  async switchIsSamlEnabled() {
+    this.switchAuthentication('isSamlEnabled', 'saml');
+  }
+
+  /**
+   * Switch Oidc enabled
+   */
+  async switchIsOidcEnabled() {
+    this.switchAuthentication('isOidcEnabled', 'oidc');
+  }
+
+  /**
+   * Switch Basic enabled
+   */
+  async switchIsBasicEnabled() {
+    this.switchAuthentication('isBasicEnabled', 'basic');
+  }
+
+  /**
+   * Switch GoogleOAuth enabled
+   */
+  async switchIsGoogleOAuthEnabled() {
+    this.switchAuthentication('isGoogleEnabled', 'google');
+  }
+
+  /**
+   * Switch GitHubOAuth enabled
+   */
+  async switchIsGitHubOAuthEnabled() {
+    this.switchAuthentication('isGitHubEnabled', 'github');
+  }
+
+  /**
+   * Switch TwitterOAuth enabled
+   */
+  async switchIsTwitterOAuthEnabled() {
+    this.switchAuthentication('isTwitterEnabled', 'twitter');
+  }
+
+}

+ 99 - 0
src/client/js/services/AdminGitHubSecurityContainer.js

@@ -0,0 +1,99 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+import { pathUtils } from 'growi-commons';
+import urljoin from 'url-join';
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+const logger = loggerFactory('growi:security:AdminGitHubSecurityContainer');
+
+/**
+ * Service container for admin security page (GitHubSecurityManagement.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminGitHubSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      appSiteUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/github/callback'),
+      githubClientId: '',
+      githubClientSecret: '',
+      isSameUsernameTreatedAsIdenticalUser: false,
+    };
+
+  }
+
+  /**
+   * retrieve security data
+   */
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { githubOAuth } = response.data.securityParams;
+      this.setState({
+        githubClientId: githubOAuth.githubClientId,
+        githubClientSecret: githubOAuth.githubClientSecret,
+        isSameUsernameTreatedAsIdenticalUser: githubOAuth.isSameUsernameTreatedAsIdenticalUser,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminGitHubSecurityContainer';
+  }
+
+  /**
+   * Change githubClientId
+   */
+  changeGitHubClientId(value) {
+    this.setState({ githubClientId: value });
+  }
+
+  /**
+   * Change githubClientSecret
+   */
+  changeGitHubClientSecret(value) {
+    this.setState({ githubClientSecret: value });
+  }
+
+  /**
+   * Switch isSameUsernameTreatedAsIdenticalUser
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Update githubSetting
+   */
+  async updateGitHubSetting() {
+    const { githubClientId, githubClientSecret, isSameUsernameTreatedAsIdenticalUser } = this.state;
+
+    let requestParams = { githubClientId, githubClientSecret, isSameUsernameTreatedAsIdenticalUser };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/github-oauth', requestParams);
+    const { securitySettingParams } = response.data;
+
+    this.setState({
+      githubClientId: securitySettingParams.githubClientId,
+      githubClientSecret: securitySettingParams.githubClientSecret,
+      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+    });
+    return response;
+  }
+
+}

+ 102 - 0
src/client/js/services/AdminGoogleSecurityContainer.js

@@ -0,0 +1,102 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+import { pathUtils } from 'growi-commons';
+import urljoin from 'url-join';
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+const logger = loggerFactory('growi:security:AdminGoogleSecurityContainer');
+
+/**
+ * Service container for admin security page (GoogleSecurityManagement.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminGoogleSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/google/callback'),
+      googleClientId: '',
+      googleClientSecret: '',
+      isSameUsernameTreatedAsIdenticalUser: false,
+    };
+
+
+  }
+
+  /**
+   * retrieve security data
+   */
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { googleOAuth } = response.data.securityParams;
+      this.setState({
+        googleClientId: googleOAuth.googleClientId,
+        googleClientSecret: googleOAuth.googleClientSecret,
+        isSameUsernameTreatedAsIdenticalUser: googleOAuth.isSameUsernameTreatedAsIdenticalUser,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminGoogleSecurityContainer';
+  }
+
+  /**
+   * Change googleClientId
+   */
+  changeGoogleClientId(value) {
+    this.setState({ googleClientId: value });
+  }
+
+  /**
+   * Change googleClientSecret
+   */
+  changeGoogleClientSecret(value) {
+    this.setState({ googleClientSecret: value });
+  }
+
+  /**
+   * Switch isSameUsernameTreatedAsIdenticalUser
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Update googleSetting
+   */
+  async updateGoogleSetting() {
+    const { googleClientId, googleClientSecret, isSameUsernameTreatedAsIdenticalUser } = this.state;
+
+    let requestParams = {
+      googleClientId, googleClientSecret, isSameUsernameTreatedAsIdenticalUser,
+    };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/google-oauth', requestParams);
+    const { securitySettingParams } = response.data;
+
+    this.setState({
+      googleClientId: securitySettingParams.googleClientId,
+      googleClientSecret: securitySettingParams.googleClientSecret,
+      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+    });
+    return response;
+  }
+
+}

+ 203 - 0
src/client/js/services/AdminLdapSecurityContainer.js

@@ -0,0 +1,203 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
+
+/**
+ * Service container for admin security page (SecurityLdapSetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminLdapSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      serverUrl: '',
+      isUserBind: false,
+      ldapBindDN: '',
+      ldapBindDNPassword: '',
+      ldapSearchFilter: '',
+      ldapAttrMapUsername: '',
+      isSameUsernameTreatedAsIdenticalUser: false,
+      ldapAttrMapMail: '',
+      ldapAttrMapName: '',
+      ldapGroupSearchBase: '',
+      ldapGroupSearchFilter: '',
+      ldapGroupDnProperty: '',
+    };
+
+  }
+
+  /**
+   * retrieve security data
+   */
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { ldapAuth } = response.data.securityParams;
+      this.setState({
+        serverUrl: ldapAuth.serverUrl,
+        isUserBind: ldapAuth.isUserBind,
+        ldapBindDN: ldapAuth.ldapBindDN,
+        ldapBindDNPassword: ldapAuth.ldapBindDNPassword,
+        ldapSearchFilter: ldapAuth.ldapSearchFilter,
+        ldapAttrMapUsername: ldapAuth.ldapAttrMapUsername,
+        isSameUsernameTreatedAsIdenticalUser: ldapAuth.isSameUsernameTreatedAsIdenticalUser,
+        ldapAttrMapMail: ldapAuth.ldapAttrMapMail,
+        ldapAttrMapName: ldapAuth.ldapAttrMapName,
+        ldapGroupSearchBase: ldapAuth.ldapGroupSearchBase,
+        ldapGroupSearchFilter: ldapAuth.ldapGroupSearchFilter,
+        ldapGroupDnProperty: ldapAuth.ldapGroupDnProperty,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+  }
+
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminLdapSecurityContainer';
+  }
+
+  /**
+   * Change serverUrl
+   */
+  changeServerUrl(serverUrl) {
+    this.setState({ serverUrl });
+  }
+
+  /**
+   * Change ldapBindMode
+   */
+  changeLdapBindMode() {
+    this.setState({ isUserBind: !this.state.isUserBind });
+  }
+
+  /**
+   * Change bindDN
+   */
+  changeBindDN(ldapBindDN) {
+    this.setState({ ldapBindDN });
+  }
+
+  /**
+   * Change bindDNPassword
+   */
+  changeBindDNPassword(ldapBindDNPassword) {
+    this.setState({ ldapBindDNPassword });
+  }
+
+  /**
+   * Change ldapSearchFilter
+   */
+  changeSearchFilter(ldapSearchFilter) {
+    this.setState({ ldapSearchFilter });
+  }
+
+  /**
+   * Change ldapAttrMapUsername
+   */
+  changeAttrMapUsername(ldapAttrMapUsername) {
+    this.setState({ ldapAttrMapUsername });
+  }
+
+  /**
+   * Switch is same username treated as identical user
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Change ldapAttrMapMail
+   */
+  changeAttrMapMail(ldapAttrMapMail) {
+    this.setState({ ldapAttrMapMail });
+  }
+
+  /**
+   * Change ldapAttrMapName
+   */
+  changeAttrMapName(ldapAttrMapName) {
+    this.setState({ ldapAttrMapName });
+  }
+
+  /**
+   * Change ldapGroupSearchBase
+   */
+  changeGroupSearchBase(ldapGroupSearchBase) {
+    this.setState({ ldapGroupSearchBase });
+  }
+
+  /**
+   * Change ldapGroupSearchFilter
+   */
+  changeGroupSearchFilter(ldapGroupSearchFilter) {
+    this.setState({ ldapGroupSearchFilter });
+  }
+
+  /**
+   * Change ldapGroupDnProperty
+   */
+  changeGroupDnProperty(ldapGroupDnProperty) {
+    this.setState({ ldapGroupDnProperty });
+  }
+
+  /**
+   * Update ldap option
+   */
+  async updateLdapSetting() {
+    const {
+      serverUrl, isUserBind, ldapBindDN, ldapBindDNPassword, ldapSearchFilter, ldapAttrMapUsername, isSameUsernameTreatedAsIdenticalUser,
+      ldapAttrMapMail, ldapAttrMapName, ldapGroupSearchBase, ldapGroupSearchFilter, ldapGroupDnProperty,
+    } = this.state;
+
+    let requestParams = {
+      serverUrl,
+      isUserBind,
+      ldapBindDN,
+      ldapBindDNPassword,
+      ldapSearchFilter,
+      ldapAttrMapUsername,
+      isSameUsernameTreatedAsIdenticalUser,
+      ldapAttrMapMail,
+      ldapAttrMapName,
+      ldapGroupSearchBase,
+      ldapGroupSearchFilter,
+      ldapGroupDnProperty,
+    };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/ldap', requestParams);
+    const { securitySettingParams } = response.data;
+
+    this.setState({
+      serverUrl: securitySettingParams.serverUrl,
+      isUserBind: securitySettingParams.isUserBind,
+      ldapBindDN: securitySettingParams.ldapBindDN,
+      ldapBindDNPassword: securitySettingParams.ldapBindDNPassword,
+      ldapSearchFilter: securitySettingParams.ldapSearchFilter,
+      ldapAttrMapUsername: securitySettingParams.ldapAttrMapUsername,
+      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      ldapAttrMapMail: securitySettingParams.ldapAttrMapMail,
+      ldapAttrMapName: securitySettingParams.ldapAttrMapName,
+      ldapGroupSearchBase: securitySettingParams.ldapGroupSearchBase,
+      ldapGroupSearchFilter: securitySettingParams.ldapGroupSearchFilter,
+      ldapGroupDnProperty: securitySettingParams.ldapGroupDnProperty,
+    });
+    return response;
+  }
+
+}

+ 85 - 0
src/client/js/services/AdminLocalSecurityContainer.js

@@ -0,0 +1,85 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:services:AdminLocalSecurityContainer');
+/**
+ * Service container for admin security page (LocalSecuritySetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminLocalSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      registrationMode: 'Open',
+      registrationWhiteList: [],
+    };
+
+  }
+
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { localSetting } = response.data.securityParams;
+      this.setState({
+        registrationMode: localSetting.registrationMode,
+        registrationWhiteList: localSetting.registrationWhiteList,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminLocalSecurityContainer';
+  }
+
+
+  /**
+   * Change registration mode
+   */
+  changeRegistrationMode(value) {
+    this.setState({ registrationMode: value });
+  }
+
+  /**
+   * Change registration white list
+   */
+  changeRegistrationWhiteList(value) {
+    this.setState({ registrationWhiteList: value.split('\n') });
+  }
+
+  /**
+   * update local security setting
+   */
+  async updateLocalSecuritySetting() {
+    const { registrationWhiteList } = this.state;
+    const response = await this.appContainer.apiv3.put('/security-setting/local-setting', {
+      registrationMode: this.state.registrationMode,
+      registrationWhiteList,
+    });
+
+    const { localSettingParams } = response.data;
+
+    this.setState({
+      registrationMode: localSettingParams.registrationMode,
+      registrationWhiteList: localSettingParams.registrationWhiteList,
+    });
+
+    return localSettingParams;
+  }
+
+
+}

+ 183 - 0
src/client/js/services/AdminOidcSecurityContainer.js

@@ -0,0 +1,183 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+import { pathUtils } from 'growi-commons';
+import urljoin from 'url-join';
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
+
+/**
+ * Service container for admin security page (OidcSecurityManagement.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminOidcSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/oidc/callback'),
+      oidcProviderName: '',
+      oidcIssuerHost: '',
+      oidcClientId: '',
+      oidcClientSecret: '',
+      oidcAttrMapId: '',
+      oidcAttrMapUserName: '',
+      oidcAttrMapName: '',
+      oidcAttrMapEmail: '',
+      isSameUsernameTreatedAsIdenticalUser: false,
+      isSameEmailTreatedAsIdenticalUser: false,
+    };
+
+  }
+
+  /**
+   * retrieve security data
+   */
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { oidcAuth } = response.data.securityParams;
+      this.setState({
+        oidcProviderName: oidcAuth.oidcProviderName,
+        oidcIssuerHost: oidcAuth.oidcIssuerHost,
+        oidcClientId: oidcAuth.oidcClientId,
+        oidcClientSecret: oidcAuth.oidcClientSecret,
+        oidcAttrMapId: oidcAuth.oidcAttrMapId,
+        oidcAttrMapUserName: oidcAuth.oidcAttrMapUserName,
+        oidcAttrMapName: oidcAuth.oidcAttrMapName,
+        oidcAttrMapEmail: oidcAuth.oidcAttrMapEmail,
+        isSameUsernameTreatedAsIdenticalUser: oidcAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser: oidcAuth.isSameEmailTreatedAsIdenticalUser,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminOidcSecurityContainer';
+  }
+
+  /**
+   * Change oidcProviderName
+   */
+  changeOidcProviderName(inputValue) {
+    this.setState({ oidcProviderName: inputValue });
+  }
+
+  /**
+   * Change oidcIssuerHost
+   */
+  changeOidcIssuerHost(inputValue) {
+    this.setState({ oidcIssuerHost: inputValue });
+  }
+
+  /**
+   * Change oidcClientId
+   */
+  changeOidcClientId(inputValue) {
+    this.setState({ oidcClientId: inputValue });
+  }
+
+  /**
+   * Change oidcClientSecret
+   */
+  changeOidcClientSecret(inputValue) {
+    this.setState({ oidcClientSecret: inputValue });
+  }
+
+  /**
+   * Change oidcAttrMapId
+   */
+  changeOidcAttrMapId(inputValue) {
+    this.setState({ oidcAttrMapId: inputValue });
+  }
+
+  /**
+   * Change oidcAttrMapUserName
+   */
+  changeOidcAttrMapUserName(inputValue) {
+    this.setState({ oidcAttrMapUserName: inputValue });
+  }
+
+  /**
+   * Change oidcAttrMapName
+   */
+  changeOidcAttrMapName(inputValue) {
+    this.setState({ oidcAttrMapName: inputValue });
+  }
+
+  /**
+   * Change oidcAttrMapEmail
+   */
+  changeOidcAttrMapEmail(inputValue) {
+    this.setState({ oidcAttrMapEmail: inputValue });
+  }
+
+  /**
+   * Switch sameUsernameTreatedAsIdenticalUser
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Switch sameEmailTreatedAsIdenticalUser
+   */
+  switchIsSameEmailTreatedAsIdenticalUser() {
+    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Update OpenID Connect
+   */
+  async updateOidcSetting() {
+    const {
+      oidcProviderName, oidcIssuerHost, oidcClientId, oidcClientSecret, oidcAttrMapId, oidcAttrMapUserName,
+      oidcAttrMapName, oidcAttrMapEmail, isSameUsernameTreatedAsIdenticalUser, isSameEmailTreatedAsIdenticalUser,
+    } = this.state;
+
+    let requestParams = {
+      oidcProviderName,
+      oidcIssuerHost,
+      oidcClientId,
+      oidcClientSecret,
+      oidcAttrMapId,
+      oidcAttrMapUserName,
+      oidcAttrMapName,
+      oidcAttrMapEmail,
+      isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser,
+    };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/oidc', requestParams);
+    const { securitySettingParams } = response.data;
+
+    this.setState({
+      oidcProviderName: securitySettingParams.oidcProviderName,
+      oidcIssuerHost: securitySettingParams.oidcIssuerHost,
+      oidcClientId: securitySettingParams.oidcClientId,
+      oidcClientSecret: securitySettingParams.oidcClientSecret,
+      oidcAttrMapId: securitySettingParams.oidcAttrMapId,
+      oidcAttrMapUserName: securitySettingParams.oidcAttrMapUserName,
+      oidcAttrMapName: securitySettingParams.oidcAttrMapName,
+      oidcAttrMapEmail: securitySettingParams.oidcAttrMapEmail,
+      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser: securitySettingParams.isSameEmailTreatedAsIdenticalUser,
+    });
+    return response;
+  }
+
+}

+ 196 - 0
src/client/js/services/AdminSamlSecurityContainer.js

@@ -0,0 +1,196 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+import { pathUtils } from 'growi-commons';
+import urljoin from 'url-join';
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+const logger = loggerFactory('growi:security:AdminSamlSecurityContainer');
+
+/**
+ * Service container for admin security page (SecuritySamlSetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminSamlSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      useOnlyEnvVars: false,
+      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/saml/callback'),
+      missingMandatoryConfigKeys: [],
+      samlEntryPoint: '',
+      samlIssuer: '',
+      samlCert: '',
+      samlAttrMapId: '',
+      samlAttrMapUsername: '',
+      samlAttrMapMail: '',
+      samlAttrMapFirstName: '',
+      samlAttrMapLastName: '',
+      isSameUsernameTreatedAsIdenticalUser: false,
+      isSameEmailTreatedAsIdenticalUser: false,
+      samlABLCRule: '',
+    };
+
+  }
+
+  /**
+   * retrieve security data
+   */
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { samlAuth } = response.data.securityParams;
+      this.setState({
+        missingMandatoryConfigKeys: samlAuth.missingMandatoryConfigKeys,
+        samlEntryPoint: samlAuth.samlEntryPoint,
+        samlIssuer: samlAuth.samlIssuer,
+        samlCert: samlAuth.samlCert,
+        samlAttrMapId: samlAuth.samlAttrMapId,
+        samlAttrMapUsername: samlAuth.samlAttrMapUsername,
+        samlAttrMapMail: samlAuth.samlAttrMapMail,
+        samlAttrMapFirstName: samlAuth.samlAttrMapFirstName,
+        samlAttrMapLastName: samlAuth.samlAttrMapLastName,
+        isSameUsernameTreatedAsIdenticalUser: samlAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser: samlAuth.isSameEmailTreatedAsIdenticalUser,
+        samlABLCRule: samlAuth.samlABLCRule,
+      });
+      return samlAuth;
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminSamlSecurityContainer';
+  }
+
+  /**
+   * Change samlEntryPoint
+   */
+  changeSamlEntryPoint(inputValue) {
+    this.setState({ samlEntryPoint: inputValue });
+  }
+
+  /**
+   * Change samlIssuer
+   */
+  changeSamlIssuer(inputValue) {
+    this.setState({ samlIssuer: inputValue });
+  }
+
+  /**
+   * Change samlCert
+   */
+  changeSamlCert(inputValue) {
+    this.setState({ samlCert: inputValue });
+  }
+
+  /**
+   * Change samlAttrMapId
+   */
+  changeSamlAttrMapId(inputValue) {
+    this.setState({ samlAttrMapId: inputValue });
+  }
+
+  /**
+   * Change samlAttrMapUsername
+   */
+  changeSamlAttrMapUserName(inputValue) {
+    this.setState({ samlAttrMapUsername: inputValue });
+  }
+
+  /**
+   * Change samlAttrMapMail
+   */
+  changeSamlAttrMapMail(inputValue) {
+    this.setState({ samlAttrMapMail: inputValue });
+  }
+
+  /**
+   * Change samlAttrMapFirstName
+   */
+  changeSamlAttrMapFirstName(inputValue) {
+    this.setState({ samlAttrMapFirstName: inputValue });
+  }
+
+  /**
+   * Change samlAttrMapLastName
+   */
+  changeSamlAttrMapLastName(inputValue) {
+    this.setState({ samlAttrMapLastName: inputValue });
+  }
+
+  /**
+   * Switch isSameUsernameTreatedAsIdenticalUser
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Switch isSameEmailTreatedAsIdenticalUser
+   */
+  switchIsSameEmailTreatedAsIdenticalUser() {
+    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Change samlABLCRule
+   */
+  changeSamlABLCRule(inputValue) {
+    this.setState({ samlABLCRule: inputValue });
+  }
+
+  /**
+   * Update saml option
+   */
+  async updateSamlSetting() {
+
+    let requestParams = {
+      entryPoint: this.state.samlEntryPoint,
+      issuer: this.state.samlIssuer,
+      cert: this.state.samlCert,
+      attrMapId: this.state.samlAttrMapId,
+      attrMapUsername: this.state.samlAttrMapUsername,
+      attrMapMail: this.state.samlAttrMapMail,
+      attrMapFirstName: this.state.samlAttrMapFirstName,
+      attrMapLastName: this.state.samlAttrMapLastName,
+      isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser: this.state.isSameEmailTreatedAsIdenticalUser,
+      ABLCRule: this.state.samlABLCRule,
+    };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/saml', requestParams);
+    const { securitySettingParams } = response.data;
+
+    this.setState({
+      missingMandatoryConfigKeys: securitySettingParams.missingMandatoryConfigKeys,
+      samlEntryPoint: securitySettingParams.samlEntryPoint,
+      samlIssuer: securitySettingParams.samlIssuer,
+      samlCert: securitySettingParams.samlCert,
+      samlAttrMapId: securitySettingParams.samlAttrMapId,
+      samlAttrMapUsername: securitySettingParams.samlAttrMapUsername,
+      samlAttrMapMail: securitySettingParams.samlAttrMapMail,
+      samlAttrMapFirstName: securitySettingParams.samlAttrMapFirstName,
+      samlAttrMapLastName: securitySettingParams.samlAttrMapLastName,
+      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser: securitySettingParams.isSameEmailTreatedAsIdenticalUser,
+      samlABLCRule: securitySettingParams.samlABLCRule,
+    });
+    return response;
+  }
+
+}

+ 99 - 0
src/client/js/services/AdminTwitterSecurityContainer.js

@@ -0,0 +1,99 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+import { pathUtils } from 'growi-commons';
+import urljoin from 'url-join';
+import removeNullPropertyFromObject from '../../../lib/util/removeNullPropertyFromObject';
+
+const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
+
+/**
+ * Service container for admin security page (TwitterSecurityManagement.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminTwitterSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/twitter/callback'),
+      twitterConsumerKey: '',
+      twitterConsumerSecret: '',
+      isSameUsernameTreatedAsIdenticalUser: false,
+    };
+
+    this.updateTwitterSetting = this.updateTwitterSetting.bind(this);
+  }
+
+  /**
+   * retrieve security data
+   */
+  async retrieveSecurityData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const { twitterOAuth } = response.data.securityParams;
+      this.setState({
+        twitterConsumerKey: twitterOAuth.twitterConsumerKey,
+        twitterConsumerSecret: twitterOAuth.twitterConsumerSecret,
+        isSameUsernameTreatedAsIdenticalUser: twitterOAuth.isSameUsernameTreatedAsIdenticalUser,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to fetch data');
+    }
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminTwitterSecurityContainer';
+  }
+
+  /**
+   * Change twitterConsumerKey
+   */
+  changeTwitterConsumerKey(value) {
+    this.setState({ twitterConsumerKey: value });
+  }
+
+  /**
+   * Change twitterConsumerSecret
+   */
+  changeTwitterConsumerSecret(value) {
+    this.setState({ twitterConsumerSecret: value });
+  }
+
+  /**
+   * Switch isSameUsernameTreatedAsIdenticalUser
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Update twitterSetting
+   */
+  async updateTwitterSetting() {
+    const { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser } = this.state;
+
+    let requestParams = { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser };
+
+    requestParams = await removeNullPropertyFromObject(requestParams);
+    const response = await this.appContainer.apiv3.put('/security-setting/twitter-oauth', requestParams);
+    const { securitySettingParams } = response.data;
+
+    this.setState({
+      twitterConsumerKey: securitySettingParams.twitterConsumerKey,
+      twitterConsumerSecret: securitySettingParams.twitterConsumerSecret,
+      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+    });
+    return response;
+  }
+
+}

+ 15 - 0
src/client/js/services/AppContainer.js

@@ -13,6 +13,10 @@ import {
   RestoreCodeBlockInterceptor,
 } from '../util/interceptor/detach-code-blocks';
 
+import {
+  DrawioInterceptor,
+} from '../util/interceptor/drawio-interceptor';
+
 import i18nFactory from '../util/i18n';
 import apiv3ErrorHandler from '../util/apiv3ErrorHandler';
 
@@ -52,6 +56,7 @@ export default class AppContainer extends Container {
 
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10); // process as soon as possible
+    this.interceptorManager.addInterceptor(new DrawioInterceptor(this), 20);
     this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
 
     const userlang = body.dataset.userlang;
@@ -299,6 +304,16 @@ export default class AppContainer extends Container {
     targetComponent.launchHandsontableModal(beginLineNumber, endLineNumber);
   }
 
+  launchDrawioModal(componentKind, beginLineNumber, endLineNumber) {
+    let targetComponent;
+    switch (componentKind) {
+      case 'page':
+        targetComponent = this.getComponentInstance('Page');
+        break;
+    }
+    targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
+  }
+
   async apiGet(path, params) {
     return this.apiRequest('get', path, { params });
   }

+ 2 - 0
src/client/js/util/GrowiRenderer.js

@@ -14,6 +14,7 @@ import TableConfigurer from './markdown-it/table';
 import TaskListsConfigurer from './markdown-it/task-lists';
 import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
 import BlockdiagConfigurer from './markdown-it/blockdiag';
+import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
 import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
 import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
 
@@ -67,6 +68,7 @@ export default class GrowiRenderer {
       new HeaderConfigurer(appContainer),
       new EmojiConfigurer(appContainer),
       new MathJaxConfigurer(appContainer),
+      new DrawioViewerConfigurer(appContainer),
       new PlantUMLConfigurer(appContainer),
       new BlockdiagConfigurer(appContainer),
     ];

+ 148 - 0
src/client/js/util/interceptor/drawio-interceptor.js

@@ -0,0 +1,148 @@
+/* eslint-disable import/prefer-default-export */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { BasicInterceptor } from 'growi-commons';
+import Drawio from '../../components/Drawio';
+
+/**
+ * The interceptor for draw.io
+ *
+ *  replace draw.io tag (render by markdown-it-drawio-viewer) to a React target element
+ */
+export class DrawioInterceptor extends BasicInterceptor {
+
+  constructor(appContainer) {
+    super();
+
+    this.previousPreviewContext = null;
+    this.appContainer = appContainer;
+
+    const DrawioViewer = window.GraphViewer;
+    if (DrawioViewer != null) {
+      // viewer.min.js の Resize による Scroll イベントを抑止するために無効化する
+      DrawioViewer.useResizeSensor = false;
+    }
+  }
+
+  /**
+   * @inheritdoc
+   */
+  isInterceptWhen(contextName) {
+    return (
+      contextName === 'preRenderHtml'
+      || contextName === 'preRenderPreviewHtml'
+      || contextName === 'postRenderHtml'
+      || contextName === 'postRenderPreviewHtml'
+    );
+  }
+
+  /**
+   * @inheritdoc
+   */
+  isProcessableParallel() {
+    return false;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async process(contextName, ...args) {
+    const context = Object.assign(args[0]); // clone
+
+    if (contextName === 'preRenderHtml' || contextName === 'preRenderPreviewHtml') {
+      return this.drawioPreRender(contextName, context);
+    }
+
+    if (contextName === 'postRenderHtml' || contextName === 'postRenderPreviewHtml') {
+      this.drawioPostRender(contextName, context);
+      return;
+    }
+  }
+
+  /**
+   * @inheritdoc
+   */
+  createRandomStr(length) {
+    const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
+    let generated = '';
+    for (let i = 0; i < length; i++) {
+      generated += bag[Math.floor(Math.random() * bag.length)];
+    }
+    return generated;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  drawioPreRender(contextName, context) {
+    const div = document.createElement('div');
+    div.innerHTML = context.parsedHTML;
+
+    context.DrawioMap = {};
+    Array.from(div.querySelectorAll('.mxgraph')).forEach((element) => {
+      const domId = `mxgraph-${this.createRandomStr(8)}`;
+
+      context.DrawioMap[domId] = {
+        rangeLineNumberOfMarkdown: {
+          beginLineNumber: element.parentNode.dataset.beginLineNumberOfMarkdown,
+          endLineNumber: element.parentNode.dataset.endLineNumberOfMarkdown,
+        },
+        contentHtml: element.outerHTML,
+      };
+      element.outerHTML = `<div id="${domId}"></div>`;
+    });
+    context.parsedHTML = div.innerHTML;
+
+    // unmount
+    if (contextName === 'preRenderPreviewHtml') {
+      this.unmountPreviousReactDOMs(context);
+    }
+
+    // resolve
+    return context;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  drawioPostRender(contextName, context) {
+    const isPreview = (contextName === 'postRenderPreviewHtml');
+
+    Object.keys(context.DrawioMap).forEach((domId) => {
+      const elem = document.getElementById(domId);
+      if (elem) {
+        this.renderReactDOM(context.DrawioMap[domId], elem, isPreview);
+      }
+    });
+  }
+
+  /**
+   * @inheritdoc
+   */
+  renderReactDOM(drawioMapEntry, elem, isPreview) {
+    ReactDOM.render(
+      // eslint-disable-next-line react/jsx-filename-extension
+      <Drawio
+        appContainer={this.appContainer}
+        drawioContent={drawioMapEntry.contentHtml}
+        isPreview={isPreview}
+        rangeLineNumberOfMarkdown={drawioMapEntry.rangeLineNumberOfMarkdown}
+      />,
+      elem,
+    );
+  }
+
+  /**
+   * @inheritdoc
+   */
+  unmountPreviousReactDOMs(newContext) {
+    if (this.previousPreviewContext != null) {
+      Array.from(document.querySelectorAll('.mxgraph')).forEach((element) => {
+        ReactDOM.unmountComponentAtNode(element);
+      });
+    }
+
+    this.previousPreviewContext = newContext;
+  }
+
+}

+ 9 - 0
src/client/js/util/markdown-it/drawio-viewer.js

@@ -0,0 +1,9 @@
+export default class DrawioViewerConfigurer {
+
+  configure(md) {
+    md.use(require('markdown-it-drawio-viewer'), {
+      marker: ':::',
+    });
+  }
+
+}

+ 3 - 0
src/client/styles/scss/_drawio.scss

@@ -0,0 +1,3 @@
+.drawio-modal {
+  @include expand-modal-fullscreen(false, false);
+}

+ 4 - 18
src/client/styles/scss/_handsontable.scss

@@ -10,25 +10,11 @@
 
 // expanded window layout
 .handsontable-modal.handsontable-modal-expanded {
-  // full-screen modal
-  width: 97%;
-  max-width: unset;
-  height: 95%;
-  .modal-content {
-    height: 95%;
-  }
-
-  // expand .modal-body (with calculating height)
-  .modal-body {
-    $modal-header: 54px;
-    $modal-footer: 46px;
-    $margin: $modal-header + $modal-footer;
-    height: calc(100% - #{$margin});
+  @include expand-modal-fullscreen(true, true);
 
-    // expand .hot-table-container (with flexbox)
-    .hot-table-container {
-      flex: 1;
-    }
+  // expand .hot-table-container (with flexbox)
+  .hot-table-container {
+    flex: 1;
   }
 }
 

+ 27 - 0
src/client/styles/scss/_mixins.scss

@@ -86,3 +86,30 @@
     }
   }
 }
+
+@mixin expand-modal-fullscreen($hasModalHeader: true, $hasModalFooter: true) {
+  // full-screen modal
+  width: auto;
+  max-width: unset;
+  height: calc(100vh - 30px);
+  margin: 15px;
+
+  .modal-content {
+    height: calc(100vh - 30px);
+  }
+
+  // expand .modal-body (with calculating height)
+  .modal-body {
+    $modal-header: 54px;
+    $modal-footer: 46px;
+
+    $margin: 0px;
+    @if $hasModalHeader {
+      $margin: $margin + $modal-header;
+    }
+    @if $hasModalFooter {
+      $margin: $margin + $modal-footer;
+    }
+    height: calc(100% - #{$margin});
+  }
+}

+ 23 - 0
src/client/styles/scss/_page.scss

@@ -48,6 +48,7 @@
 .main .content-main .revision-history {
   .revision-history-list {
     .revision-history-outer {
+
       // add border-top except of first element
       &:not(:first-of-type) {
         border-top: 1px solid $border;
@@ -140,6 +141,28 @@
   }
 }
 
+/**
+ * for drawio with drawio iframe button
+ */
+.editable-with-drawio {
+  .drawio-iframe-trigger {
+    top: 11px;
+    right: 10px;
+    z-index: 14;
+    font-size: 12px;
+    line-height: 1;
+    color: $linktext;
+    background-color: transparent;
+    border: 1px solid $linktext;
+    opacity: 1;
+
+    &:hover {
+      color: $white;
+      background-color: $linktext;
+    }
+  }
+}
+
 /*
  * for Presentation
  */

+ 2 - 0
src/client/styles/scss/style-app.scss

@@ -26,6 +26,8 @@
 @import 'comment_crowi';
 @import 'comment_growi';
 @import 'comment_kibela';
+@import 'drawio';
+@import 'navbar_kibela';
 @import 'create-page';
 @import 'create-template';
 @import 'draft';

+ 12 - 0
src/lib/util/removeNullPropertyFromObject.js

@@ -0,0 +1,12 @@
+// remove property if value is null
+
+const removeNullPropertyFromObject = (object) => {
+
+  for (const [key, value] of Object.entries(object)) {
+    if (value == null) { delete object[key] }
+  }
+
+  return object;
+};
+
+module.exports = removeNullPropertyFromObject;

+ 12 - 12
src/server/crowi/index.js

@@ -130,19 +130,19 @@ Crowi.prototype.initForTest = async function() {
   ]);
 
   await Promise.all([
-  //   this.scanRuntimeVersions(),
-  //   this.setupPassport(),
-  //   this.setupSearcher(),
-  //   this.setupMailer(),
-  //   this.setupSlack(),
-  //   this.setupCsrf(),
-  //   this.setUpFileUpload(),
+    // this.scanRuntimeVersions(),
+    this.setupPassport(),
+    // this.setupSearcher(),
+    // this.setupMailer(),
+    // this.setupSlack(),
+    // this.setupCsrf(),
+    // this.setUpFileUpload(),
     this.setUpAcl(),
-  //   this.setUpCustomize(),
-  //   this.setUpRestQiitaAPI(),
-  //   this.setupUserGroup(),
-  //   this.setupExport(),
-  //   this.setupImport(),
+    // this.setUpCustomize(),
+    // this.setUpRestQiitaAPI(),
+    // this.setupUserGroup(),
+    // this.setupExport(),
+    // this.setupImport(),
   ]);
 
   // globalNotification depends on slack and mailer

+ 0 - 7
src/server/form/admin/custombehavior.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:behavior]'),
-);

+ 0 - 7
src/server/form/admin/customcss.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:css]'),
-);

+ 0 - 12
src/server/form/admin/customfeatures.js

@@ -1,12 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:isEnabledTimeline]').trim().toBooleanStrict(),
-  field('settingForm[customize:isEnabledDeleteCompletely]').trim().toBooleanStrict(),
-  field('settingForm[customize:isSavedStatesOfTabChanges]').trim().toBooleanStrict(),
-  field('settingForm[customize:isEnabledAttachTitleHeader]').trim().toBooleanStrict(),
-  field('settingForm[customize:showRecentCreatedNumber]').trim().toInt(),
-  field('settingForm[customize:isEnabledStaleNotification]').trim().toBooleanStrict(),
-);

+ 0 - 7
src/server/form/admin/customheader.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:header]'),
-);

+ 0 - 8
src/server/form/admin/customhighlightJsStyle.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:highlightJsStyle]'),
-  field('settingForm[customize:highlightJsStyleBorder]').trim().toBooleanStrict(),
-);

+ 0 - 8
src/server/form/admin/customlayout.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:layout]'),
-  field('settingForm[customize:theme]'),
-);

+ 0 - 7
src/server/form/admin/customscript.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:script]'),
-);

+ 0 - 7
src/server/form/admin/customtheme.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:theme]'),
-);

+ 0 - 7
src/server/form/admin/customtitle.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[customize:title]'),
-);

+ 0 - 10
src/server/form/admin/securityGeneral.js

@@ -1,10 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:restrictGuestMode]'),
-  field('settingForm[security:list-policy:hideRestrictedByOwner]').trim().toBooleanStrict(),
-  field('settingForm[security:list-policy:hideRestrictedByGroup]').trim().toBooleanStrict(),
-  field('settingForm[security:pageCompleteDeletionAuthority]'),
-);

+ 0 - 8
src/server/form/admin/securityPassportBasic.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:passport-basic:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:passport-basic:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-);

+ 0 - 10
src/server/form/admin/securityPassportGitHub.js

@@ -1,10 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:passport-github:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:passport-github:clientId]').trim(),
-  field('settingForm[security:passport-github:clientSecret]').trim(),
-  field('settingForm[security:passport-github:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-);

+ 0 - 10
src/server/form/admin/securityPassportGoogle.js

@@ -1,10 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:passport-google:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:passport-google:clientId]').trim(),
-  field('settingForm[security:passport-google:clientSecret]').trim(),
-  field('settingForm[security:passport-google:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-);

+ 0 - 23
src/server/form/admin/securityPassportLdap.js

@@ -1,23 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:passport-ldap:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:passport-ldap:serverUrl]').trim()
-  // https://regex101.com/r/E0UL6D/1
-    .is(/^ldaps?:\/\/([^/\s]+)\/([^/\s]+)$/, 'Server URL is invalid. <small><a href="https://regex101.com/r/E0UL6D/1">&gt;&gt; Regex</a></small>'),
-  field('settingForm[security:passport-ldap:isUserBind]').trim().toBooleanStrict(),
-  field('settingForm[security:passport-ldap:bindDN]').trim()
-  // https://regex101.com/r/jK8lpO/1
-    .is(/^(,?[^,=\s]+=[^,=\s]+){1,}$|^[^@\s]+@[^@\s]+$/, 'Bind DN is invalid. <small><a href="https://regex101.com/r/jK8lpO/3">&gt;&gt; Regex</a></small>'),
-  field('settingForm[security:passport-ldap:bindDNPassword]'),
-  field('settingForm[security:passport-ldap:searchFilter]'),
-  field('settingForm[security:passport-ldap:attrMapUsername]'),
-  field('settingForm[security:passport-ldap:attrMapName]'),
-  field('settingForm[security:passport-ldap:attrMapMail]'),
-  field('settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-  field('settingForm[security:passport-ldap:groupSearchBase]'),
-  field('settingForm[security:passport-ldap:groupSearchFilter]'),
-  field('settingForm[security:passport-ldap:groupDnProperty]'),
-);

+ 0 - 11
src/server/form/admin/securityPassportLocal.js

@@ -1,11 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-const stringToArray = require('../../util/formUtil').stringToArrayFilter;
-const normalizeCRLF = require('../../util/formUtil').normalizeCRLFFilter;
-
-module.exports = form(
-  field('settingForm[security:passport-local:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:registrationMode]').required(),
-  field('settingForm[security:registrationWhiteList]').custom(normalizeCRLF).custom(stringToArray),
-);

+ 0 - 17
src/server/form/admin/securityPassportOidc.js

@@ -1,17 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:passport-oidc:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:passport-oidc:providerName]').trim(),
-  field('settingForm[security:passport-oidc:issuerHost]').trim(),
-  field('settingForm[security:passport-oidc:clientId]').trim(),
-  field('settingForm[security:passport-oidc:clientSecret]').trim(),
-  field('settingForm[security:passport-oidc:attrMapId]').trim(),
-  field('settingForm[security:passport-oidc:attrMapUserName]').trim(),
-  field('settingForm[security:passport-oidc:attrMapName]').trim(),
-  field('settingForm[security:passport-oidc:attrMapMail]').trim(),
-  field('settingForm[security:passport-oidc:isSameEmailTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-  field('settingForm[security:passport-oidc:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-);

+ 0 - 18
src/server/form/admin/securityPassportSaml.js

@@ -1,18 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:passport-saml:isEnabled]').trim().toBooleanStrict(),
-  field('settingForm[security:passport-saml:entryPoint]').trim().isUrl(),
-  field('settingForm[security:passport-saml:issuer]').trim(),
-  field('settingForm[security:passport-saml:attrMapId]').trim(),
-  field('settingForm[security:passport-saml:attrMapUsername]').trim(),
-  field('settingForm[security:passport-saml:attrMapMail]').trim(),
-  field('settingForm[security:passport-saml:attrMapFirstName]').trim(),
-  field('settingForm[security:passport-saml:attrMapLastName]').trim(),
-  field('settingForm[security:passport-saml:cert]').trim(),
-  field('settingForm[security:passport-saml:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-  field('settingForm[security:passport-saml:isSameEmailTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-  field('settingForm[security:passport-saml:ABLCRule]').trim(),
-);

+ 0 - 10
src/server/form/admin/securityPassportTwitter.js

@@ -1,10 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:passport-twitter:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:passport-twitter:consumerKey]').trim(),
-  field('settingForm[security:passport-twitter:consumerSecret]').trim(),
-  field('settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
-);

+ 0 - 9
src/server/form/index.js

@@ -11,15 +11,6 @@ module.exports = {
     apiToken: require('./me/apiToken'),
   },
   admin: {
-    securityGeneral: require('./admin/securityGeneral'),
-    securityPassportLocal: require('./admin/securityPassportLocal'),
-    securityPassportLdap: require('./admin/securityPassportLdap'),
-    securityPassportSaml: require('./admin/securityPassportSaml'),
-    securityPassportBasic: require('./admin/securityPassportBasic'),
-    securityPassportGoogle: require('./admin/securityPassportGoogle'),
-    securityPassportGitHub: require('./admin/securityPassportGitHub'),
-    securityPassportTwitter: require('./admin/securityPassportTwitter'),
-    securityPassportOidc: require('./admin/securityPassportOidc'),
     userGroupCreate: require('./admin/userGroupCreate'),
   },
 };

+ 15 - 9
src/server/models/config.js

@@ -66,11 +66,26 @@ module.exports = function(crowi) {
       'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': false,
       'security:passport-saml:isEnabled' : false,
       'security:passport-saml:isSameEmailTreatedAsIdenticalUser': false,
+
       'security:passport-google:isEnabled' : false,
+      'security:passport-google:clientId': undefined,
+      'security:passport-google:clientSecret': undefined,
+      'security:passport-google:isSameUsernameTreatedAsIdenticalUser': false,
+
       'security:passport-github:isEnabled' : false,
+      'security:passport-github:clientId': undefined,
+      'security:passport-github:clientSecret': undefined,
+      'security:passport-github:isSameUsernameTreatedAsIdenticalUser': false,
+
       'security:passport-twitter:isEnabled' : false,
+      'security:passport-twitter:consumerKey': undefined,
+      'security:passport-twitter:consumerSecret': undefined,
+      'security:passport-twitter:isSameUsernameTreatedAsIdenticalUser': false,
+
       'security:passport-oidc:isEnabled' : false,
+
       'security:passport-basic:isEnabled' : false,
+      'security:passport-basic:isSameUsernameTreatedAsIdenticalUser': false,
 
       'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
@@ -84,15 +99,6 @@ module.exports = function(crowi) {
       'mail:smtpUser'     : undefined,
       'mail:smtpPassword' : undefined,
 
-      'security:passport-google:clientId'     : undefined,
-      'security:passport-google:clientSecret' : undefined,
-
-      'security:passport-github:clientId': undefined,
-      'security:passport-github:clientSecret': undefined,
-
-      'security:passport-twitter:clientId': undefined,
-      'security:passport-twitter:clientSecret': undefined,
-
       'plugin:isEnabledPlugins' : true,
 
       'customize:css' : undefined,

+ 13 - 256
src/server/routes/admin.js

@@ -405,242 +405,6 @@ module.exports = function(crowi, app) {
   };
 
   actions.api = {};
-  actions.api.securitySetting = async function(req, res) {
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    const form = req.form.settingForm;
-    if (aclService.isWikiModeForced()) {
-      logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
-      delete form['security:restrictGuestMode'];
-    }
-
-    try {
-      await configManager.updateConfigsInTheSameNamespace('crowi', form);
-      return res.json({ status: true });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json({ status: false });
-    }
-  };
-
-  actions.api.securityPassportLocalSetting = async function(req, res) {
-    const form = req.form.settingForm;
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-
-    try {
-      await configManager.updateConfigsInTheSameNamespace('crowi', form);
-      // reset strategy
-      crowi.passportService.resetLocalStrategy();
-      // setup strategy
-      if (configManager.getConfig('crowi', 'security:passport-local:isEnabled')) {
-        crowi.passportService.setupLocalStrategy(true);
-      }
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json({ status: false, message: err.message });
-    }
-
-    return res.json({ status: true });
-  };
-
-  actions.api.securityPassportLdapSetting = async function(req, res) {
-    const form = req.form.settingForm;
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-
-    try {
-      await configManager.updateConfigsInTheSameNamespace('crowi', form);
-      // reset strategy
-      crowi.passportService.resetLdapStrategy();
-      // setup strategy
-      if (configManager.getConfig('crowi', 'security:passport-ldap:isEnabled')) {
-        crowi.passportService.setupLdapStrategy(true);
-      }
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json({ status: false, message: err.message });
-    }
-
-    return res.json({ status: true });
-  };
-
-  actions.api.securityPassportSamlSetting = async(req, res) => {
-    const form = req.form.settingForm;
-
-    validateSamlSettingForm(req.form, req.t);
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-    await configManager.updateConfigsInTheSameNamespace('crowi', form);
-
-    // reset strategy
-    await crowi.passportService.resetSamlStrategy();
-    // setup strategy
-    if (configManager.getConfig('crowi', 'security:passport-saml:isEnabled')) {
-      try {
-        await crowi.passportService.setupSamlStrategy(true);
-      }
-      catch (err) {
-        // reset
-        await crowi.passportService.resetSamlStrategy();
-        return res.json({ status: false, message: err.message });
-      }
-    }
-
-    return res.json({ status: true });
-  };
-
-  actions.api.securityPassportBasicSetting = async(req, res) => {
-    const form = req.form.settingForm;
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-    await configManager.updateConfigsInTheSameNamespace('crowi', form);
-
-    // reset strategy
-    await crowi.passportService.resetBasicStrategy();
-    // setup strategy
-    if (configManager.getConfig('crowi', 'security:passport-basic:isEnabled')) {
-      try {
-        await crowi.passportService.setupBasicStrategy(true);
-      }
-      catch (err) {
-        // reset
-        await crowi.passportService.resetBasicStrategy();
-        return res.json({ status: false, message: err.message });
-      }
-    }
-
-    return res.json({ status: true });
-  };
-
-  actions.api.securityPassportGoogleSetting = async(req, res) => {
-    const form = req.form.settingForm;
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-    await configManager.updateConfigsInTheSameNamespace('crowi', form);
-
-    // reset strategy
-    await crowi.passportService.resetGoogleStrategy();
-    // setup strategy
-    if (configManager.getConfig('crowi', 'security:passport-google:isEnabled')) {
-      try {
-        await crowi.passportService.setupGoogleStrategy(true);
-      }
-      catch (err) {
-        // reset
-        await crowi.passportService.resetGoogleStrategy();
-        return res.json({ status: false, message: err.message });
-      }
-    }
-
-    return res.json({ status: true });
-  };
-
-  actions.api.securityPassportGitHubSetting = async(req, res) => {
-    const form = req.form.settingForm;
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-    await configManager.updateConfigsInTheSameNamespace('crowi', form);
-
-    // reset strategy
-    await crowi.passportService.resetGitHubStrategy();
-    // setup strategy
-    if (configManager.getConfig('crowi', 'security:passport-github:isEnabled')) {
-      try {
-        await crowi.passportService.setupGitHubStrategy(true);
-      }
-      catch (err) {
-        // reset
-        await crowi.passportService.resetGitHubStrategy();
-        return res.json({ status: false, message: err.message });
-      }
-    }
-
-    return res.json({ status: true });
-  };
-
-  actions.api.securityPassportTwitterSetting = async(req, res) => {
-    const form = req.form.settingForm;
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-    await configManager.updateConfigsInTheSameNamespace('crowi', form);
-
-    // reset strategy
-    await crowi.passportService.resetTwitterStrategy();
-    // setup strategy
-    if (configManager.getConfig('crowi', 'security:passport-twitter:isEnabled')) {
-      try {
-        await crowi.passportService.setupTwitterStrategy(true);
-      }
-      catch (err) {
-        // reset
-        await crowi.passportService.resetTwitterStrategy();
-        return res.json({ status: false, message: err.message });
-      }
-    }
-
-    return res.json({ status: true });
-  };
-
-  actions.api.securityPassportOidcSetting = async(req, res) => {
-    const form = req.form.settingForm;
-
-    if (!req.form.isValid) {
-      return res.json({ status: false, message: req.form.errors.join('\n') });
-    }
-
-    debug('form content', form);
-    await configManager.updateConfigsInTheSameNamespace('crowi', form);
-
-    // reset strategy
-    await crowi.passportService.resetOidcStrategy();
-    // setup strategy
-    if (configManager.getConfig('crowi', 'security:passport-oidc:isEnabled')) {
-      try {
-        await crowi.passportService.setupOidcStrategy(true);
-      }
-      catch (err) {
-        // reset
-        await crowi.passportService.resetOidcStrategy();
-        return res.json({ status: false, message: err.message });
-      }
-    }
-
-    return res.json({ status: true });
-  };
 
   // app.get('/_api/admin/users.search' , admin.api.userSearch);
   actions.api.usersSearch = function(req, res) {
@@ -778,29 +542,22 @@ module.exports = function(crowi, app) {
     }
   };
 
-  /**
-   * validate setting form values for SAML
-   *
-   * - For the value of each mandatory items,
-   *     check whether it from the environment variables is empty and form value to update it is empty
-   * - validate the syntax of a attribute-based login control rule
-   */
-  function validateSamlSettingForm(form, t) {
-    for (const key of crowi.passportService.mandatoryConfigKeysForSaml) {
-      const formValue = form.settingForm[key];
-      if (configManager.getConfigFromEnvVars('crowi', key) === null && formValue === '') {
-        const formItemName = t(`security_setting.form_item_name.${key}`);
-        form.errors.push(t('form_validation.required', formItemName));
-      }
+
+  actions.api.searchBuildIndex = async function(req, res) {
+    const search = crowi.getSearcher();
+    if (!search) {
+      return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
     }
 
-    const rule = form.settingForm['security:passport-saml:ABLCRule'];
-    // Empty string disables attribute-based login control.
-    // So, when rule is empty string, validation is passed.
-    if (rule !== '' && (rule == null || crowi.passportService.parseABLCRule(rule) == null)) {
-      form.errors.push(t('form_validation.invalid_syntax', t('security_setting.form_item_name.security:passport-saml:ABLCRule')));
+    try {
+      search.buildIndex();
     }
-  }
+    catch (err) {
+      return res.json(ApiResponse.error(err));
+    }
+
+    return res.json(ApiResponse.success());
+  };
 
   return actions;
 };

+ 2 - 0
src/server/routes/apiv3/index.js

@@ -37,6 +37,8 @@ module.exports = (crowi) => {
 
   router.use('/statistics', require('./statistics')(crowi));
 
+  router.use('/security-setting', require('./security-setting')(crowi));
+
   router.use('/search', require('./search')(crowi));
 
   return router;

+ 985 - 0
src/server/routes/apiv3/security-setting.js

@@ -0,0 +1,985 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:security-setting');
+
+const express = require('express');
+
+const router = express.Router();
+
+const { body } = require('express-validator/check');
+const ErrorV3 = require('../../models/vo/error-apiv3');
+const removeNullPropertyFromObject = require('../../../lib/util/removeNullPropertyFromObject');
+
+const validator = {
+  generalSetting: [
+    body('restrictGuestMode').isString().isIn([
+      'Deny', 'Readonly',
+    ]),
+    body('pageCompleteDeletionAuthority').isString().isIn([
+      'anyOne', 'adminOnly', 'adminAndAuthor',
+    ]),
+    body('hideRestrictedByOwner').if((value, { req }) => req.body.hideRestrictedByOwner).isBoolean(),
+    body('hideRestrictedByGroup').if((value, { req }) => req.body.hideRestrictedByGroup).isBoolean(),
+  ],
+  authenticationSetting: [
+    body('isEnabled').if((value, { req }) => req.body.isEnabled).isBoolean(),
+    body('authId').isString().isIn([
+      'local', 'ldap', 'saml', 'oidc', 'basic', 'google', 'github', 'twitter',
+    ]),
+  ],
+  localSetting: [
+    body('registrationMode').isString().isIn([
+      'Open', 'Restricted', 'Closed',
+    ]),
+    body('registrationWhiteList').if((value, { req }) => req.body.registrationWhiteList).isArray().customSanitizer((value, { req }) => {
+      return value.filter(email => email !== '');
+    }),
+  ],
+  ldapAuth: [
+    body('serverUrl').if((value, { req }) => req.body.serverUrl).isString(),
+    body('isUserBind').if((value, { req }) => req.body.isUserBind).isBoolean(),
+    body('ldapBindDN').if((value, { req }) => req.body.ldapBindDN).isString(),
+    body('ldapBindDNPassword').if((value, { req }) => req.body.ldapBindDNPassword).isString(),
+    body('ldapSearchFilter').if((value, { req }) => req.body.ldapSearchFilter).isString(),
+    body('ldapAttrMapUsername').if((value, { req }) => req.body.ldapAttrMapUsername).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+    body('ldapAttrMapMail').if((value, { req }) => req.body.ldapAttrMapMail).isString(),
+    body('ldapAttrMapName').if((value, { req }) => req.body.ldapAttrMapName).isString(),
+    body('ldapGroupSearchBase').if((value, { req }) => req.body.ldapGroupSearchBase).isString(),
+    body('ldapGroupSearchFilter').if((value, { req }) => req.body.ldapGroupSearchFilter).isString(),
+    body('ldapGroupDnProperty').if((value, { req }) => req.body.ldapGroupDnProperty).isString(),
+  ],
+  samlAuth: [
+    body('entryPoint').if((value, { req }) => req.body.samlEntryPoint).isString(),
+    body('issuer').if((value, { req }) => req.body.samlIssuer).isString(),
+    body('cert').if((value, { req }) => req.body.samlCert).isString(),
+    body('attrMapId').if((value, { req }) => req.body.samlAttrMapId).isString(),
+    body('attrMapUsername').if((value, { req }) => req.body.samlAttrMapUsername).isString(),
+    body('attrMapMail').if((value, { req }) => req.body.samlAttrMapMail).isString(),
+    body('attrMapFirstName').if((value, { req }) => req.body.samlAttrMapFirstName).isString(),
+    body('attrMapLastName').if((value, { req }) => req.body.samlAttrMapLastName).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+    body('isSameEmailTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameEmailTreatedAsIdenticalUser).isBoolean(),
+    body('ABLCRule').if((value, { req }) => req.body.samlABLCRule).isString(),
+  ],
+  oidcAuth: [
+    body('oidcProviderName').if((value, { req }) => req.body.oidcProviderName).isString(),
+    body('oidcIssuerHost').if((value, { req }) => req.body.oidcIssuerHost).isString(),
+    body('oidcClientId').if((value, { req }) => req.body.oidcClientId).isString(),
+    body('oidcClientSecret').if((value, { req }) => req.body.oidcClientSecret).isString(),
+    body('oidcAttrMapId').if((value, { req }) => req.body.oidcAttrMapId).isString(),
+    body('oidcAttrMapUserName').if((value, { req }) => req.body.oidcAttrMapUserName).isString(),
+    body('oidcAttrMapEmail').if((value, { req }) => req.body.oidcAttrMapEmail).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+    body('isSameEmailTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameEmailTreatedAsIdenticalUser).isBoolean(),
+  ],
+  basicAuth: [
+    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+  ],
+  googleOAuth: [
+    body('googleClientId').if((value, { req }) => req.body.googleClientId).isString(),
+    body('googleClientSecret').if((value, { req }) => req.body.googleClientSecret).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+  ],
+  githubOAuth: [
+    body('githubClientId').if((value, { req }) => req.body.githubClientId).isString(),
+    body('githubClientSecret').if((value, { req }) => req.body.githubClientSecret).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+  ],
+  twitterOAuth: [
+    body('twitterConsumerKey').if((value, { req }) => req.body.twitterConsumerKey).isString(),
+    body('twitterConsumerSecret').if((value, { req }) => req.body.twitterConsumerSecret).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+  ],
+};
+
+/**
+ * @swagger
+ *  tags:
+ *    name: SecuritySetting
+ */
+
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      GeneralSetting:
+ *        type: object
+ *        properties:
+ *          restrictGuestMode:
+ *            type: string
+ *            description: type of restrictGuestMode
+ *          pageCompleteDeletionAuthority:
+ *            type: string
+ *            description: type of pageDeletionAuthority
+ *          hideRestrictedByOwner:
+ *            type: boolean
+ *            description: enable hide by owner
+ *          hideRestrictedByGroup:
+ *            type: boolean
+ *            description: enable hide by group
+ *      LocalSetting:
+ *        type: object
+ *        properties:
+ *          isLocalEnabled:
+ *            type: boolean
+ *            description: local setting mode
+ *          registrationMode:
+ *            type: string
+ *            description: type of registrationMode
+ *          registrationWhiteList:
+ *            type: array
+ *            description: array of regsitrationList
+ *            items:
+ *              type: string
+ *              description: registration whiteList
+ *      LdapAuthSetting:
+ *        type: object
+ *        properties:
+ *          serverUrl:
+ *            type: string
+ *            description: server url for ldap
+ *          isUserBind:
+ *            type: boolean
+ *            description: enable user bind
+ *          ldapBindDN:
+ *            type: string
+ *            description: the query used to bind with the directory service
+ *          ldapBindDNPassword:
+ *            type: string
+ *            description: the password that is entered in the login page will be used to bind
+ *          ldapSearchFilter:
+ *            type: string
+ *            description: the query used to locate the authenticated user
+ *          ldapAttrMapUsername:
+ *            type: string
+ *            description: specification of mappings for username when creating new users
+ *          isSameUsernameTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the user name matched
+ *          ldapAttrMapMail:
+ *            type: string
+ *            description: specification of mappings for mail address when creating new users
+ *          ldapAttrMapName:
+ *            type: string
+ *            description: Specification of mappings for full name address when creating new users
+ *          ldapGroupSearchBase:
+ *            type: string
+ *            description: the base DN from which to search for groups.
+ *          ldapGroupSearchFilter:
+ *            type: string
+ *            description: the query used to filter for groups
+ *          ldapGroupDnProperty:
+ *            type: string
+ *            description: The property of user object to use in dn interpolation of Group Search Filter
+ *      SamlAuthSetting:
+ *        type: object
+ *        properties:
+ *          samlEntryPoint:
+ *            type: string
+ *            description: entry point for saml
+ *          samlIssuer:
+ *            type: string
+ *            description: issuer for saml
+ *          samlCert:
+ *            type: string
+ *            description: certificate for saml
+ *          samlAttrMapId:
+ *            type: string
+ *            description: attribute mapping id for saml
+ *          samlAttrMapUserName:
+ *            type: string
+ *            description: attribute mapping user name for saml
+ *          samlAttrMapMail:
+ *            type: string
+ *            description: attribute mapping mail for saml
+ *          samlAttrMapFirstName:
+ *            type: string
+ *            description: attribute mapping first name for saml
+ *          samlAttrMapLastName:
+ *            type: string
+ *            description: attribute mapping last name for saml
+ *          isSameUsernameTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the user name matched
+ *          isSameEmailTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the email matched
+ *          samlABLCRule:
+ *            type: string
+ *            description: ABLCRule for saml
+ *      OidcAuthSetting:
+ *        type: object
+ *        properties:
+ *          oidcProviderName:
+ *            type: string
+ *            description: provider name for oidc
+ *          oidcIssuerHost:
+ *            type: string
+ *            description: issuer host for oidc
+ *          oidcClientId:
+ *            type: string
+ *            description: client id for oidc
+ *          oidcClientSecret:
+ *            type: string
+ *            description: client secret for oidc
+ *          oidcAttrMapId:
+ *            type: string
+ *            description: attr map id for oidc
+ *          oidcAttrMapUserName:
+ *            type: string
+ *            description: attr map username for oidc
+ *          oidcAttrMapName:
+ *            type: string
+ *            description: attr map name for oidc
+ *          oidcAttrMapMail:
+ *            type: string
+ *            description: attr map mail for oidc
+ *          isSameUsernameTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the user name matched
+ *          isSameEmailTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the email matched
+ *      BasicAuthSetting:
+ *        type: object
+ *        properties:
+ *          isSameUsernameTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the email matched
+ *      GitHubOAuthSetting:
+ *        type: object
+ *        properties:
+ *          githubClientId:
+ *            type: string
+ *            description: key of comsumer
+ *          githubClientSecret:
+ *            type: string
+ *            description: password of comsumer
+ *          isSameUsernameTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the email matched
+ *      GoogleOAuthSetting:
+ *        type: object
+ *        properties:
+ *          googleClientId:
+ *            type: string
+ *            description: key of comsumer
+ *          googleClientSecret:
+ *            type: string
+ *            description: password of comsumer
+ *          isSameUsernameTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the email matched
+ *      TwitterOAuthSetting:
+ *        type: object
+ *        properties:
+ *          twitterConsumerKey:
+ *            type: string
+ *            description: key of comsumer
+ *          twitterConsumerSecret:
+ *            type: string
+ *            description: password of comsumer
+ *          isSameUsernameTreatedAsIdenticalUser:
+ *            type: boolean
+ *            description: local account automatically linked the email matched
+ */
+module.exports = (crowi) => {
+  const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
+  const adminRequired = require('../../middleware/admin-required')(crowi);
+  const csrf = require('../../middleware/csrf')(crowi);
+
+  const { ApiV3FormValidator } = crowi.middlewares;
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/:
+   *      get:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Get security paramators
+   *        responses:
+   *          200:
+   *            description: params of security
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    securityParams:
+   *                      type: object
+   *                      description: security params
+   */
+  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+
+    const securityParams = {
+      generalSetting: {
+        restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
+        pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
+        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'),
+      },
+      localSetting: {
+        registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
+        registrationWhiteList: await crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
+      },
+      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'),
+        isBasicEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-basic:isEnabled'),
+        isGoogleEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-google:isEnabled'),
+        isGitHubEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-github:isEnabled'),
+        isTwitterEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-twitter: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'),
+      },
+      samlAuth: {
+        missingMandatoryConfigKeys: await crowi.passportService.getSamlMissingMandatoryConfigKeys(),
+        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.getConfig('crowi', 'security:passport-saml:ABLCRule'),
+        samlEnvVarABLCRule: await crowi.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'),
+        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'),
+      },
+      basicAuth: {
+        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser'),
+      },
+      googleOAuth: {
+        googleClientId: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientId'),
+        googleClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
+        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-google:isSameUsernameTreatedAsIdenticalUser'),
+      },
+      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'),
+      },
+      twitterOAuth: {
+        twitterConsumerKey: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:consumerKey'),
+        twitterConsumerSecret: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:consumerSecret'),
+        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:isSameUsernameTreatedAsIdenticalUser'),
+      },
+    };
+    return res.apiv3({ securityParams });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/authentication/enabled:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update authentication isEnabled
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  isEnabled:
+   *                    type: boolean
+   *                  target:
+   *                    type: string
+   *        responses:
+   *          200:
+   *            description: Succeeded to enable authentication
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  type: object
+   *                  description: updated param
+   */
+  router.put('/authentication/enabled', loginRequiredStrictly, adminRequired, csrf, validator.authenticationSetting, ApiV3FormValidator, async(req, res) => {
+    const { isEnabled, authId } = req.body;
+
+    let setupStrategies = await crowi.passportService.getSetupStrategies();
+
+    // Reflect request param
+    setupStrategies = setupStrategies.filter(strategy => strategy !== authId);
+
+    if (setupStrategies.length === 0) {
+      return res.apiv3Err(new ErrorV3('Can not turn everything off'), 405);
+    }
+
+    const enableParams = { [`security:passport-${authId}:isEnabled`]: isEnabled };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', enableParams);
+
+      await crowi.passportService.setupStrategyById(authId);
+
+      const responseParams = {
+        [`security:passport-${authId}:isEnabled`]: await crowi.configManager.getConfig('crowi', `security:passport-${authId}:isEnabled`),
+      };
+
+      return res.apiv3({ responseParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating enable setting';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-enable-setting failed'));
+    }
+
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/authentication:
+   *      get:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Get setup strategies for passport
+   *        responses:
+   *          200:
+   *            description: params of setup strategies
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    setupStrategies:
+   *                      type: array
+   *                      description: setup strategies list
+   *                      items:
+   *                        type: string
+   *                        description: setup strategie
+   *                      example: ["local"]
+   */
+  router.get('/authentication/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const setupStrategies = await crowi.passportService.getSetupStrategies();
+
+    return res.apiv3({ setupStrategies });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/general-setting:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update GeneralSetting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/GeneralSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update general Setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/GeneralSetting'
+   */
+  router.put('/general-setting', loginRequiredStrictly, adminRequired, csrf, validator.generalSetting, ApiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'security:restrictGuestMode': req.body.restrictGuestMode,
+      'security:pageCompleteDeletionAuthority': req.body.pageCompleteDeletionAuthority,
+      'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
+      'security:list-policy:hideRestrictedByGroup': req.body.hideRestrictedByGroup,
+    };
+    const wikiMode = await crowi.configManager.getConfig('crowi', 'security:wikiMode');
+    if (wikiMode === 'private') {
+      logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
+      delete requestParams['security:restrictGuestMode'];
+    }
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      const securitySettingParams = {
+        restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
+        pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
+        hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
+        hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating security setting';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-secuirty-setting failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/local-setting:
+   *      put:
+   *        tags: [LocalSetting, apiv3]
+   *        description: Update LocalSetting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/LocalSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update local Setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/LocalSetting'
+   */
+  router.put('/local-setting', loginRequiredStrictly, adminRequired, csrf, validator.localSetting, ApiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'security:registrationMode': req.body.registrationMode,
+      'security:registrationWhiteList': req.body.registrationWhiteList,
+    };
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('local');
+      const localSettingParams = {
+        registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
+        registrationWhiteList: await crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
+      };
+      return res.apiv3({ localSettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating local setting';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-local-setting failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/ldap:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update LDAP setting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/LdapAuthSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update LDAP setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/LdapAuthSetting'
+   */
+  router.put('/ldap', loginRequiredStrictly, adminRequired, csrf, validator.ldapAuth, ApiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'security:passport-ldap:serverUrl': req.body.serverUrl,
+      'security:passport-ldap:isUserBind': req.body.isUserBind,
+      'security:passport-ldap:bindDN': req.body.ldapBindDN,
+      'security:passport-ldap:bindDNPassword': req.body.ldapBindDNPassword,
+      'security:passport-ldap:searchFilter': req.body.ldapSearchFilter,
+      'security:passport-ldap:attrMapUsername': req.body.ldapAttrMapUsername,
+      'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
+      'security:passport-ldap:attrMapMail': req.body.ldapAttrMapMail,
+      'security:passport-ldap:attrMapName': req.body.ldapAttrMapName,
+      'security:passport-ldap:groupSearchBase': req.body.ldapGroupSearchBase,
+      'security:passport-ldap:groupSearchFilter': req.body.ldapGroupSearchFilter,
+      'security:passport-ldap:groupDnProperty': req.body.ldapGroupDnProperty,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('ldap');
+      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'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating SAML setting';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-SAML-failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/saml:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update SAML setting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/SamlAuthSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update SAML setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/SamlAuthSetting'
+   */
+  router.put('/saml', loginRequiredStrictly, adminRequired, csrf, validator.samlAuth, ApiV3FormValidator, async(req, res) => {
+
+    //  For the value of each mandatory items,
+    //  check whether it from the environment variables is empty and form value to update it is empty
+    //  validate the syntax of a attribute - based login control rule
+    const invalidValues = [];
+    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) {
+        const formItemName = req.t(`security_setting.form_item_name.${key}`);
+        invalidValues.push(req.t('form_validation.required', formItemName));
+      }
+    }
+    if (invalidValues.length !== 0) {
+      return res.apiv3Err(req.t('form_validation.error_message'), 400, invalidValues);
+    }
+
+    const rule = req.body.ABLCRule;
+    // Empty string disables attribute-based login control.
+    // So, when rule is empty string, validation is passed.
+    if (rule != null) {
+      try {
+        crowi.passportService.parseABLCRule(rule);
+      }
+      catch (err) {
+        return res.apiv3Err(req.t('form_validation.invalid_syntax', req.t('security_setting.form_item_name.ABLCRule')), 400);
+      }
+    }
+
+    const requestParams = {
+      'security:passport-saml:entryPoint': req.body.entryPoint,
+      'security:passport-saml:issuer': req.body.issuer,
+      'security:passport-saml:cert': req.body.cert,
+      'security:passport-saml:attrMapId': req.body.attrMapId,
+      'security:passport-saml:attrMapUsername': req.body.attrMapUsername,
+      'security:passport-saml:attrMapMail': req.body.attrMapMail,
+      'security:passport-saml:attrMapFirstName': req.body.attrMapFirstName,
+      'security:passport-saml:attrMapLastName': req.body.attrMapLastName,
+      'security:passport-saml:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
+      'security:passport-saml:isSameEmailTreatedAsIdenticalUser': req.body.isSameEmailTreatedAsIdenticalUser,
+      'security:passport-saml:ABLCRule': req.body.ABLCRule,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('saml');
+      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'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating SAML setting';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-SAML-failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/oidc:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update OpenID Connect setting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/OidcAuthSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update OpenID Connect setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/OidcAuthSetting'
+   */
+  router.put('/oidc', loginRequiredStrictly, adminRequired, csrf, validator.oidcAuth, ApiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'security:passport-oidc:providerName': req.body.oidcProviderName,
+      'security:passport-oidc:issuerHost': req.body.oidcIssuerHost,
+      'security:passport-oidc:clientId': req.body.oidcClientId,
+      'security:passport-oidc:clientSecret': req.body.oidcClientSecret,
+      'security:passport-oidc:attrMapId': req.body.oidcAttrMapId,
+      'security:passport-oidc:attrMapUserName': req.body.oidcAttrMapUserName,
+      'security:passport-oidc:attrMapName': req.body.oidcAttrMapName,
+      'security:passport-oidc:attrMapMail': req.body.oidcAttrMapEmail,
+      'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
+      'security:passport-oidc:isSameEmailTreatedAsIdenticalUser': req.body.isSameEmailTreatedAsIdenticalUser,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('oidc');
+      const securitySettingParams = {
+        oidcProviderName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
+        oidcIssuerHost: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
+        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'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating OpenIDConnect';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-OpenIDConnect-failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/basic:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update basic
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/BasicAuthSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update basic
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/BasicAuthSetting'
+   */
+  router.put('/basic', loginRequiredStrictly, adminRequired, csrf, validator.basicAuth, ApiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'security:passport-basic:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('basic');
+      const securitySettingParams = {
+        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating basicAuth';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-basicOAuth-failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/google-oauth:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update google OAuth
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/GoogleOAuthSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to google OAuth
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/GoogleOAuthSetting'
+   */
+  router.put('/google-oauth', loginRequiredStrictly, adminRequired, csrf, validator.googleOAuth, ApiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'security:passport-google:clientId': req.body.googleClientId,
+      'security:passport-google:clientSecret': req.body.googleClientSecret,
+      'security:passport-google:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('google');
+      const securitySettingParams = {
+        googleClientId: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientId'),
+        googleClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
+        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-google:isSameUsernameTreatedAsIdenticalUser'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating googleOAuth';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-googleOAuth-failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/github-oauth:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update github OAuth
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/GitHubOAuthSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to github OAuth
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/GitHubOAuthSetting'
+   */
+  router.put('/github-oauth', loginRequiredStrictly, adminRequired, csrf, validator.githubOAuth, ApiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'security:passport-github:clientId': req.body.githubClientId,
+      'security:passport-github:clientSecret': req.body.githubClientSecret,
+      'security:passport-github:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('github');
+      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'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      // reset strategy
+      await crowi.passportService.resetGitHubStrategy();
+      const msg = 'Error occurred in updating githubOAuth';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-githubOAuth-failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/twitter-oauth:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update twitter OAuth
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/TwitterOAuthSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update twitter OAuth
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/TwitterOAuthSetting'
+   */
+  router.put('/twitter-oauth', loginRequiredStrictly, adminRequired, csrf, validator.twitterOAuth, ApiV3FormValidator, async(req, res) => {
+
+    let requestParams = {
+      'security:passport-twitter:consumerKey': req.body.twitterConsumerKey,
+      'security:passport-twitter:consumerSecret': req.body.twitterConsumerSecret,
+      'security:passport-twitter:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
+    };
+
+    requestParams = removeNullPropertyFromObject(requestParams);
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.passportService.setupStrategyById('twitter');
+      const securitySettingParams = {
+        twitterConsumerId: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:consumerKey'),
+        twitterConsumerSecret: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:consumerSecret'),
+        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:isSameUsernameTreatedAsIdenticalUser'),
+      };
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating twitterOAuth';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-twitterOAuth-failed'));
+    }
+  });
+
+  return router;
+};

+ 0 - 9
src/server/routes/index.js

@@ -62,17 +62,8 @@ module.exports = function(crowi, app) {
 
   // security admin
   app.get('/admin/security'                     , loginRequiredStrictly , adminRequired , admin.security.index);
-  app.post('/_api/admin/security/general'       , loginRequiredStrictly , adminRequired , form.admin.securityGeneral, admin.api.securitySetting);
-  app.post('/_api/admin/security/passport-local', loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportLocal, admin.api.securityPassportLocalSetting);
-  app.post('/_api/admin/security/passport-ldap' , loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportLdap, admin.api.securityPassportLdapSetting);
-  app.post('/_api/admin/security/passport-saml' , loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportSaml, admin.api.securityPassportSamlSetting);
-  app.post('/_api/admin/security/passport-basic', loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportBasic, admin.api.securityPassportBasicSetting);
 
   // OAuth
-  app.post('/_api/admin/security/passport-google' , loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportGoogle, admin.api.securityPassportGoogleSetting);
-  app.post('/_api/admin/security/passport-github' , loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportGitHub, admin.api.securityPassportGitHubSetting);
-  app.post('/_api/admin/security/passport-twitter', loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportTwitter, admin.api.securityPassportTwitterSetting);
-  app.post('/_api/admin/security/passport-oidc',    loginRequiredStrictly , adminRequired , csrf, form.admin.securityPassportOidc, admin.api.securityPassportOidcSetting);
   app.get('/passport/google'                      , loginPassport.loginWithGoogle);
   app.get('/passport/github'                      , loginPassport.loginWithGitHub);
   app.get('/passport/twitter'                     , loginPassport.loginWithTwitter);

+ 12 - 12
src/server/routes/installer.js

@@ -20,23 +20,22 @@ module.exports = function(crowi, app) {
     await searchService.rebuildIndex();
   }
 
+  async function createPage(filePath, pagePath, owner, lang) {
+    const markdown = fs.readFileSync(filePath);
+    return Page.create(pagePath, markdown, owner, {});
+  }
+
   async function createInitialPages(owner, lang) {
     const promises = [];
 
     // create portal page for '/'
-    const welcomeMarkdownPath = path.join(crowi.localeDir, lang, 'welcome.md');
-    const welcomeMarkdown = fs.readFileSync(welcomeMarkdownPath);
-    promises.push(Page.create('/', welcomeMarkdown, owner, {}));
-
-    // create /Sandbox
-    const sandboxMarkdownPath = path.join(crowi.localeDir, lang, 'sandbox.md');
-    const sandboxMarkdown = fs.readFileSync(sandboxMarkdownPath);
-    promises.push(Page.create('/Sandbox', sandboxMarkdown, owner, {}));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'welcome.md'), '/', owner, lang));
 
-    // create /Sandbox/Bootstrap3
-    const bs3MarkdownPath = path.join(crowi.localeDir, 'en-US', 'sandbox-bootstrap3.md');
-    const bs3Markdown = fs.readFileSync(bs3MarkdownPath);
-    promises.push(Page.create('/Sandbox/Bootstrap3', bs3Markdown, owner, {}));
+    // create /Sandbox/*
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox.md'), '/Sandbox', owner, lang));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-bootstrap3.md'), '/Sandbox/Bootstrap3', owner, lang));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner, lang));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner, lang));
 
     await Promise.all(promises);
 
@@ -68,6 +67,7 @@ module.exports = function(crowi, app) {
     await appService.initDB(language);
 
     // create first admin user
+    // TODO: with transaction
     let adminUser;
     try {
       adminUser = await User.createUser(name, username, email, password, language);

+ 11 - 10
src/server/routes/login-passport.js

@@ -6,6 +6,7 @@ module.exports = function(crowi, app) {
   const passport = require('passport');
   const ExternalAccount = crowi.model('ExternalAccount');
   const passportService = crowi.passportService;
+  const ApiResponse = require('../util/apiResponse');
 
   /**
    * success handler
@@ -129,10 +130,10 @@ module.exports = function(crowi, app) {
   const testLdapCredentials = (req, res) => {
     if (!passportService.isLdapStrategySetup) {
       debug('LdapStrategy has not been set up');
-      return res.json({
+      return res.json(ApiResponse.success({
         status: 'warning',
         message: 'LdapStrategy has not been set up',
-      });
+      }));
     }
 
     passport.authenticate('ldapauth', (err, user, info) => {
@@ -142,36 +143,36 @@ module.exports = function(crowi, app) {
 
       if (err) { // DB Error
         logger.error('LDAP Server Error: ', err);
-        return res.json({
+        return res.json(ApiResponse.success({
           status: 'warning',
           message: 'LDAP Server Error occured.',
           err,
-        });
+        }));
       }
       if (info && info.message) {
-        return res.json({
+        return res.json(ApiResponse.success({
           status: 'warning',
           message: info.message,
           ldapConfiguration: req.ldapConfiguration,
           ldapAccountInfo: req.ldapAccountInfo,
-        });
+        }));
       }
       if (user) {
         // check groups
         if (!isValidLdapUserByGroupFilter(user)) {
-          return res.json({
+          return res.json(ApiResponse.success({
             status: 'warning',
             message: 'This user does not belong to any groups designated by the group search filter.',
             ldapConfiguration: req.ldapConfiguration,
             ldapAccountInfo: req.ldapAccountInfo,
-          });
+          }));
         }
-        return res.json({
+        return res.json(ApiResponse.success({
           status: 'success',
           message: 'Successfully authenticated.',
           ldapConfiguration: req.ldapConfiguration,
           ldapAccountInfo: req.ldapAccountInfo,
-        });
+        }));
       }
     })(req, res, () => {});
   };

+ 154 - 87
src/server/service/passport.js

@@ -1,5 +1,6 @@
 const debug = require('debug')('growi:service:PassportService');
 const urljoin = require('url-join');
+const luceneQueryParser = require('lucene-query-parser');
 const passport = require('passport');
 const LocalStrategy = require('passport-local').Strategy;
 const LdapStrategy = require('passport-ldapauth');
@@ -73,7 +74,6 @@ class PassportService {
      * the keys of mandatory configs for SAML
      */
     this.mandatoryConfigKeysForSaml = [
-      'security:passport-saml:isEnabled',
       'security:passport-saml:entryPoint',
       'security:passport-saml:issuer',
       'security:passport-saml:cert',
@@ -81,6 +81,88 @@ class PassportService {
       'security:passport-saml:attrMapUsername',
       'security:passport-saml:attrMapMail',
     ];
+
+    this.setupFunction = {
+      local: {
+        setup: 'setupLocalStrategy',
+        reset: 'resetLocalStrategy',
+      },
+      ldap: {
+        setup: 'setupLdapStrategy',
+        reset: 'resetLdapStrategy',
+      },
+      saml: {
+        setup: 'setupSamlStrategy',
+        reset: 'resetSamlStrategy',
+      },
+      oidc: {
+        setup: 'setupOidcStrategy',
+        reset: 'resetOidcStrategy',
+      },
+      basic: {
+        setup: 'setupBasicStrategy',
+        reset: 'resetBasicStrategy',
+      },
+      google: {
+        setup: 'setupGoogleStrategy',
+        reset: 'resetGoogleStrategy',
+      },
+      github: {
+        setup: 'setupGitHubStrategy',
+        reset: 'resetGitHubStrategy',
+      },
+      twitter: {
+        setup: 'setupTwitterStrategy',
+        reset: 'resetTwitterStrategy',
+      },
+    };
+  }
+
+  /**
+   * get SetupStrategies
+   *
+   * @return {Array}
+   * @memberof PassportService
+   */
+  getSetupStrategies() {
+    const setupStrategies = [];
+
+    if (this.isLocalStrategySetup) { setupStrategies.push('local') }
+    if (this.isLdapStrategySetup) { setupStrategies.push('ldap') }
+    if (this.isSamlStrategySetup) { setupStrategies.push('saml') }
+    if (this.isOidcStrategySetup) { setupStrategies.push('oidc') }
+    if (this.isBasicStrategySetup) { setupStrategies.push('basic') }
+    if (this.isGoogleStrategySetup) { setupStrategies.push('google') }
+    if (this.isGitHubStrategySetup) { setupStrategies.push('github') }
+    if (this.isTwitterStrategySetup) { setupStrategies.push('twitter') }
+
+    return setupStrategies;
+  }
+
+  /**
+   * get SetupFunction
+   *
+   * @return {Object}
+   * @param {string} authId
+   */
+  getSetupFunction(authId) {
+    return this.setupFunction[authId];
+  }
+
+  /**
+   * setup strategy by target name
+   */
+  setupStrategyById(authId) {
+    const func = this.getSetupFunction(authId);
+
+    try {
+      this[func.setup]();
+    }
+    catch (err) {
+      debug(err);
+      this[func.reset]();
+    }
+
   }
 
   /**
@@ -100,10 +182,8 @@ class PassportService {
    * @memberof PassportService
    */
   setupLocalStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isLocalStrategySetup) {
-      throw new Error('LocalStrategy has already been set up');
-    }
+
+    this.resetLocalStrategy();
 
     const { configManager } = this.crowi;
 
@@ -157,10 +237,8 @@ class PassportService {
    * @memberof PassportService
    */
   setupLdapStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isLdapStrategySetup) {
-      throw new Error('LdapStrategy has already been set up');
-    }
+
+    this.resetLdapStrategy();
 
     const config = this.crowi.config;
     const { configManager } = this.crowi;
@@ -323,10 +401,8 @@ class PassportService {
    * @memberof PassportService
    */
   setupGoogleStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isGoogleStrategySetup) {
-      throw new Error('GoogleStrategy has already been set up');
-    }
+
+    this.resetGoogleStrategy();
 
     const { configManager } = this.crowi;
     const isGoogleEnabled = configManager.getConfig('crowi', 'security:passport-google:isEnabled');
@@ -373,10 +449,8 @@ class PassportService {
   }
 
   setupGitHubStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isGitHubStrategySetup) {
-      throw new Error('GitHubStrategy has already been set up');
-    }
+
+    this.resetGitHubStrategy();
 
     const { configManager } = this.crowi;
     const isGitHubEnabled = configManager.getConfig('crowi', 'security:passport-github:isEnabled');
@@ -423,10 +497,8 @@ class PassportService {
   }
 
   setupTwitterStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isTwitterStrategySetup) {
-      throw new Error('TwitterStrategy has already been set up');
-    }
+
+    this.resetTwitterStrategy();
 
     const { configManager } = this.crowi;
     const isTwitterEnabled = configManager.getConfig('crowi', 'security:passport-twitter:isEnabled');
@@ -473,10 +545,8 @@ class PassportService {
   }
 
   async setupOidcStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isOidcStrategySetup) {
-      throw new Error('OidcStrategy has already been set up');
-    }
+
+    this.resetOidcStrategy();
 
     const { configManager } = this.crowi;
     const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
@@ -536,10 +606,8 @@ class PassportService {
   }
 
   setupSamlStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isSamlStrategySetup) {
-      throw new Error('SamlStrategy has already been set up');
-    }
+
+    this.resetSamlStrategy();
 
     const { configManager } = this.crowi;
     const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
@@ -598,6 +666,18 @@ class PassportService {
     return missingRequireds;
   }
 
+  /**
+   * Parse Attribute-Based Login Control Rule as Lucene Query
+   * @param {string} rule Lucene syntax string
+   * @returns {object} Expression Tree Structure generated by lucene-query-parser
+   * @see https://github.com/thoward/lucene-query-parser.js/wiki
+   */
+  parseABLCRule(rule) {
+    // parse with lucene-query-parser
+    // see https://github.com/thoward/lucene-query-parser.js/wiki
+    return luceneQueryParser.parse(rule);
+  }
+
   /**
    * Verify that a SAML response meets the attribute-base login control rule
    */
@@ -607,73 +687,62 @@ class PassportService {
       return true;
     }
 
-    const expr = this.parseABLCRule(rule);
-    if (expr == null) {
-      return false;
-    }
-    debug({ 'Parsed Rule': JSON.stringify(expr, null, 2) });
+    const luceneRule = this.parseABLCRule(rule);
+    debug({ 'Parsed Rule': JSON.stringify(luceneRule, null, 2) });
 
     const attributes = this.extractAttributesFromSAMLResponse(response);
     debug({ 'Extracted Attributes': JSON.stringify(attributes, null, 2) });
 
-    let evaluatedExpr = false;
-    for (const orOp of expr) {
-      let evaluatedOrOp = true;
-      for (const andOp of orOp) {
-        if (attributes[andOp[0]] == null) {
-          evaluatedOrOp = false;
-          break;
-        }
-        evaluatedOrOp = evaluatedOrOp && attributes[andOp[0]].includes(andOp[1]);
-      }
-      evaluatedExpr = evaluatedExpr || evaluatedOrOp;
+    return this.evaluateRuleForSamlAttributes(attributes, luceneRule);
+  }
+
+  /**
+   * Evaluate whether the specified rule is satisfied under the specified attributes
+   *
+   * @param {object} attributes results by extractAttributesFromSAMLResponse
+   * @param {object} luceneRule Expression Tree Structure generated by lucene-query-parser
+   * @see https://github.com/thoward/lucene-query-parser.js/wiki
+   */
+  evaluateRuleForSamlAttributes(attributes, luceneRule) {
+    const { left, right, operator } = luceneRule;
+
+    // when combined rules
+    if (right != null) {
+      return this.evaluateCombinedRulesForSamlAttributes(attributes, left, right, operator);
+    }
+    if (left != null) {
+      return this.evaluateRuleForSamlAttributes(attributes, left);
     }
 
-    return evaluatedExpr;
+    const { field, term } = luceneRule;
+    if (field === '<implicit>') {
+      return attributes[term] != null;
+    }
+    return attributes[field].includes(term);
   }
 
   /**
-   * Parse a rule string for the attribute-based login control
+   * Evaluate whether the specified two rules are satisfied under the specified attributes
    *
-   * The syntax rules are as follows.
-   * <attr> and <value> are any characters except "|", "&", "=".
-   *
-   * ## Syntax
-   *    <expr>   ::= <or_op> | <or_op> "|" <expr>
-   *    <or_op>  ::= <and_op> | <and_op> "&" <or_op>
-   *    <and_op> ::= <attr> "=" <value>
-   *
-   * ## Example
-   *  In:  "Department = A | Department = B & Position = Leader"
-   *  Out:
-   *    [
-   *      [
-   *        ["Department", "A"]
-   *      ],
-   *      [
-   *        ["Department","B"],
-   *        ["Position","Leader"]
-   *      ]
-   *    ]
-   *
-   *   In:  Invalid syntax string like a "This is a & bad & rule string."
-   *   Out: null
+   * @param {object} attributes results by extractAttributesFromSAMLResponse
+   * @param {object} luceneRuleLeft Expression Tree Structure generated by lucene-query-parser
+   * @param {object} luceneRuleRight Expression Tree Structure generated by lucene-query-parser
+   * @param {string} luceneOperator operator string expression
+   * @see https://github.com/thoward/lucene-query-parser.js/wiki
    */
-  parseABLCRule(rule) {
-    let expr = rule.split('|');
-    expr = expr.map(orOp => orOp.trim().split('&'));
-    expr = expr.map(orOp => orOp.map(andOp => andOp.trim().split('=')));
-    expr = expr.map(orOp => orOp.map(andOp => andOp.map(v => v.trim())));
-    for (const orOp of expr) {
-      for (const andOp of orOp) {
-        if (andOp.length !== 2) {
-          return null;
-        }
-      }
+  evaluateCombinedRulesForSamlAttributes(attributes, luceneRuleLeft, luceneRuleRight, luceneOperator) {
+    if (luceneOperator === 'OR') {
+      return this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) || this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight);
+    }
+    if (luceneOperator === 'AND') {
+      return this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) && this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight);
+    }
+    if (luceneOperator === 'NOT') {
+      return this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) && !this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight);
     }
-    return expr;
-  }
 
+    throw new Error(`Unsupported operator: ${luceneOperator}`);
+  }
 
   /**
    * Extract attributes from a SAML response
@@ -729,10 +798,8 @@ class PassportService {
    * @memberof PassportService
    */
   setupBasicStrategy() {
-    // check whether the strategy has already been set up
-    if (this.isBasicStrategySetup) {
-      throw new Error('BasicStrategy has already been set up');
-    }
+
+    this.resetBasicStrategy();
 
     const configManager = this.crowi.configManager;
     const isBasicEnabled = configManager.getConfig('crowi', 'security:passport-basic:isEnabled');

+ 1 - 255
src/server/views/admin/security.html

@@ -13,261 +13,7 @@
 {% block content_main %}
 <div class="content-main admin-security row">
   {% parent %}
-    <div class="col-md-9">
-
-      {% set smessage = req.flash('successMessage') %}
-      {% if smessage.length %}
-      <div class="alert alert-success">
-        {% for e in smessage %}
-          {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      {% set emessage = req.flash('errorMessage') %}
-      {% if emessage.length %}
-      <div class="alert alert-danger">
-        {% for e in emessage %}
-        {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <form action="/_api/admin/security/general" method="post" class="form-horizontal" id="generalSetting" role="form">
-        <fieldset>
-        <legend class="alert-anchor">{{ t('security_settings') }}</legend>
-
-          <div class="form-group">
-            <label for="settingForm[security:restrictGuestMode]" class="col-xs-3 control-label">{{ t('security_setting.Guest Users Access') }}</label>
-            <div class="col-xs-6">
-              {% set selectedValue = guestModeValue %}
-              <select class="form-control selectpicker" {% if isWikiModeForced %}disabled{% endif %}
-                  name="settingForm[security:restrictGuestMode]" value="{{ getConfig('crowi', 'security:restrictGuestMode') }}">
-                {% for modeValue, modeLabel in consts.restrictGuestMode %}
-                  <option value="{{ t(modeValue) }}" {% if modeValue == selectedValue %}selected{% endif %}>{{ t(modeLabel) }}</option>
-                {% endfor %}
-              </select>
-              {% if isWikiModeForced %}
-              <p class="alert alert-warning mt-2">
-                <i class="icon-exclamation icon-fw"></i><b>FIXED</b><br>
-                {{ t('security_setting.Fixed by env var', 'FORCE_WIKI_MODE', getConfig('crowi', 'security:wikiMode')) }}
-              </p>
-              {% endif %}
-            </div>
-          </div>
-
-          <div class="form-group">
-            {% set configName = 'settingForm[security:list-policy:hideRestrictedByOwner]' %}
-            {% set configValue = getConfig('crowi', 'security:list-policy:hideRestrictedByOwner') %}
-            {% set isEnabled = !configValue %}
-            <label for="{{configName}}" class="col-xs-3 control-label">{{ t("security_setting.page_listing_1") }}</label>
-            <div class="col-xs-9">
-              <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default btn-rounded btn-outline {% if isEnabled %}active{% endif %}" data-active-class="primary">
-                  <input name="{{configName}}" value="false" type="radio" {% if isEnabled %}checked{% endif %}> ON
-                </label>
-                <label class="btn btn-default btn-rounded btn-outline {% if !isEnabled %}active{% endif %}" data-active-class="default">
-                  <input name="{{configName}}" value="true" type="radio" {% if !isEnabled %}checked{% endif %}> OFF
-                </label>
-              </div>
-
-              <p class="help-block small">
-                {{ t("security_setting.page_listing_1_desc") }}
-              </p>
-            </div>
-          </div>
-
-          <div class="form-group">
-            {% set configName = 'settingForm[security:list-policy:hideRestrictedByGroup]' %}
-            {% set configValue = getConfig('crowi', 'security:list-policy:hideRestrictedByGroup') %}
-            {% set isEnabled = !configValue %}
-            <label for="{{configName}}" class="col-xs-3 control-label">{{ t("security_setting.page_listing_2") }}</label>
-            <div class="col-xs-9">
-              <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default btn-rounded btn-outline {% if isEnabled %}active{% endif %}" data-active-class="primary">
-                  <input name="{{configName}}" value="false" type="radio" {% if isEnabled %}checked{% endif %}> ON
-                </label>
-                <label class="btn btn-default btn-rounded btn-outline {% if !isEnabled %}active{% endif %}" data-active-class="default">
-                  <input name="{{configName}}" value="true" type="radio" {% if !isEnabled %}checked{% endif %}> OFF
-                </label>
-              </div>
-
-              <p class="help-block small">
-                {{ t("security_setting.page_listing_2_desc") }}
-              </p>
-            </div>
-          </div>
-
-          <div class="form-group">
-            {% set configName = 'settingForm[security:pageCompleteDeletionAuthority]' %}
-            {% set configValue = getConfig('crowi','security:pageCompleteDeletionAuthority') %}
-            <label for="{{configName}}" class="col-xs-3 control-label">{{ t('security_setting.complete_deletion') }}</label>
-            <div class="col-xs-6">
-              <select class="form-control selectpicker" name="settingForm[security:pageCompleteDeletionAuthority]" value="{{ configValue }}">
-                <option value="anyOne" {% if configValue == "anyOne" %}selected{% endif %}>{{ t('security_setting.anyone') }}</option>
-                <option value="adminOnly" {% if configValue =="adiminOnly" %}selected{% endif %}>{{ t('security_setting.admin_only') }}</option>
-                <option value="adminAndAuthor" {% if configValue == "adminAndAuthor" %}selected{% endif %}>{{ t('security_setting.admin_and_author') }}</option>
-              </select>
-
-              <p class="help-block small">
-                {{ t('security_setting.complete_deletion_explain') }}
-              </p>
-            </div>
-          </div>
-
-          <div class="form-group">
-            <div class="col-xs-offset-3 col-xs-6">
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-            </div>
-          </div>
-
-        </fieldset>
-      </form>
-
-      <!-- prevent XSS link -->
-      <div class="mt-5">
-        <legend>{{ t('security_setting.xss_prevent_setting') }}</legend>
-        <div class="text-center">
-          <a class="flexbox" style="font-size: large;" href="/admin/markdown/#preventXSS">
-            <i class="fa-fw icon-login"></i> {{ t('security_setting.xss_prevent_setting_link') }}
-          </a>
-        </div>
-       </div>
-
-
-      <div class="auth-mechanism-configurations grw-mt-10px">
-
-        <legend>{{ t('security_setting.Authentication mechanism settings') }}</legend>
-
-        {#
-         # passport settings nav
-         #}
-        <div class="passport-settings">
-          <ul class="nav nav-tabs" role="tablist">
-            <li class="active">
-              <a href="#passport-local" data-toggle="tab" role="tab"><i class="fa fa-users"></i> ID/Pass</a>
-            </li>
-            <li>
-              <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="fa fa-sitemap"></i> LDAP</a>
-            </li>
-            <li>
-              <a href="#passport-saml" data-toggle="tab" role="tab"><i class="fa fa-key"></i> SAML</a>
-            </li>
-            <li>
-              <a href="#passport-oidc" data-toggle="tab" role="tab"><i class="fa fa-openid"></i> OIDC</a>
-            </li>
-            <li>
-              <a href="#passport-basic" data-toggle="tab" role="tab"><i class="fa fa-lock"></i> Basic</a>
-            </li>
-            <li>
-              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="fa fa-google"></i> Google</a>
-            </li>
-            <li>
-              <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> GitHub</a>
-            </li>
-            <li>
-              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> Twitter</a>
-            </li>
-            <li class="tbd">
-              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
-            </li>
-          </ul>
-
-          <div class="tab-content grw-pt-10px">
-            <div id="passport-local" class="tab-pane active" role="tabpanel" >
-              {% include './widget/passport/local.html' %}
-            </div>
-
-            <div id="passport-ldap" class="tab-pane" role="tabpanel" >
-              {% include './widget/passport/ldap.html' with { settingForm: settingForm } %}
-            </div>
-
-            <div id="passport-saml" class="tab-pane" role="tabpanel" >
-              {% include './widget/passport/saml.html' %}
-            </div>
-
-            <div id="passport-oidc" class="tab-pane" role="tabpanel">
-              {% include './widget/passport/oidc.html' %}
-            </div>
-
-            <div id="passport-basic" class="tab-pane" role="tabpanel">
-              {% include './widget/passport/basic.html' %}
-            </div>
-
-            <div id="passport-google-oauth" class="tab-pane" role="tabpanel">
-              {% include './widget/passport/google-oauth.html' %}
-            </div>
-
-            <div id="passport-facebook" class="tab-pane" role="tabpanel">
-              {% include './widget/passport/facebook.html' %}
-            </div>
-
-            <div id="passport-twitter" class="tab-pane" role="tabpanel">
-              {% include './widget/passport/twitter.html' %}
-            </div>
-
-            <div id="passport-github" class="tab-pane" role="tabpanel">
-              {% include './widget/passport/github.html' %}
-            </div>
-
-          </div><!-- /.tab-content -->
-        </div>
-
-      </div><!-- /.auth-mechanism-configurations -->
-    </div>
-  </div>
-
-  <script>
-    $('#generalSetting, #localSetting, #samlSetting, #basicSetting, #googleSetting, #githubSetting, #twitterSetting, #oidcSetting').each(function() {
-      $(this).submit(function()
-      {
-        function showMessage(formId, msg, status) {
-          $('#' + formId + ' > .alert').remove();
-          $('#' + formId ).find('.alert').remove();
-
-          if (!status) {
-            status = 'success';
-          }
-          var $message = $('<p class="alert"></p>');
-          $message.addClass('alert-' + status);
-          $message.html(msg.replace(/\n/g, '<br>'));
-          $message.insertAfter('#' + formId + ' .alert-anchor');
-
-          if (status == 'success') {
-            setTimeout(function()
-            {
-              $message.fadeOut({
-                complete: function() {
-                  $message.remove();
-                }
-              });
-            }, 5000);
-          }
-        }
-
-        var $form = $(this);
-        var $id = $form.attr('id');
-        var $button = $('button', this);
-        $button.attr('disabled', 'disabled');
-        var jqxhr = $.post($form.attr('action'), $form.serialize(), function(data)
-          {
-            if (data.status) {
-              showMessage($id, '更新しました Updated');
-            } else {
-              showMessage($id, data.message, 'danger');
-            }
-          })
-          .fail(function() {
-            showMessage($id, 'エラーが発生しました Error', 'danger');
-          })
-          .always(function() {
-            $button.prop('disabled', false);
-        });
-        return false;
-      });
-    });
-  </script>
+  <div class="col-md-9" id="admin-security-setting"></div>
 </div>
 {% endblock content_main %}
 

+ 0 - 73
src/server/views/admin/widget/passport/basic.html

@@ -1,73 +0,0 @@
-<form action="/_api/admin/security/passport-basic" method="post" class="form-horizontal passportStrategy" id="basicSetting" role="form"
-    {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
-  <legend class="alert-anchor">{{ t("security_setting.Basic.name") }} {{ t("security_setting.configuration") }}</legend>
-
-  {% set nameForIsbasicEnabled = "settingForm[security:passport-basic:isEnabled]" %}
-  {% set isbasicEnabled = getConfig('crowi', 'security:passport-basic:isEnabled') %}
-
-  <div class="form-group">
-    <label for="{{nameForIsbasicEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.Basic.name") }}</label>
-    <div class="col-xs-6">
-      <div class="btn-group btn-toggle" data-toggle="buttons">
-        <label class="btn btn-default btn-rounded btn-outline {% if isbasicEnabled %}active{% endif %}" data-active-class="primary">
-          <input name="{{nameForIsbasicEnabled}}" value="true" type="radio"
-              {% if true === isbasicEnabled %}checked{% endif %}> ON
-        </label>
-        <label class="btn btn-default btn-rounded btn-outline {% if !isbasicEnabled %}active{% endif %}" data-active-class="default">
-          <input name="{{nameForIsbasicEnabled}}" value="false" type="radio"
-              {% if !isbasicEnabled %}checked{% endif %}> OFF
-        </label>
-      </div>
-      <p class="help-block">
-        <small>
-          {{ t("security_setting.Basic.desc_1") }}<br>
-          {{ t("security_setting.Basic.desc_2") }}
-        </small>
-      </p>
-    </div>
-  </div>
-
-
-  <fieldset id="passport-basic-hide-when-disabled" {%if !isbasicEnabled %}style="display: none;"{% endif %}>
-
-    <div class="form-group">
-    <div class="col-xs-6 col-xs-offset-3">
-      <div class="checkbox checkbox-info">
-        <input type="checkbox" id="bindByUserName-basic" name="settingForm[security:passport-basic:isSameUsernameTreatedAsIdenticalUser]" value="1"
-            {% if getConfig('crowi', 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser') %}checked{% endif %} />
-        <label for="bindByUserName-basic">
-          {{ t("security_setting.Treat username matching as identical", "username") }}
-        </label>
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.Treat username matching as identical_warn", "username") }}
-          </small>
-        </p>
-    </div>
-      </div>
-    </div>
-
-  </fieldset>
-
-  <div class="form-group" id="btn-update">
-    <div class="col-xs-offset-3 col-xs-6">
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-    </div>
-  </div>
-
-</form>
-
-<script>
-  $('input[name="settingForm[security:passport-basic:isEnabled]"]').change(function() {
-    const isEnabled = ($(this).val() === "true");
-
-    if (isEnabled) {
-      $('#passport-basic-hide-when-disabled').show(400);
-    }
-    else {
-      $('#passport-basic-hide-when-disabled').hide(400);
-    }
-  });
-</script>
-

+ 0 - 6
src/server/views/admin/widget/passport/facebook.html

@@ -1,6 +0,0 @@
-<form action="" method="post" class="form-horizontal passportStrategy" id="facebookOauthSetting" role="form">
-  <fieldset>
-    <legend>Facebook OAuth {{ t("security_setting.configuration") }}</legend>
-    <p class="well">(TBD)</p>
-  </fieldset>
-</form>

+ 0 - 120
src/server/views/admin/widget/passport/github.html

@@ -1,120 +0,0 @@
-<form action="/_api/admin/security/passport-github" method="post" class="form-horizontal passportStrategy" id="githubSetting" role="form">
-  <legend class="alert-anchor">{{ t("security_setting.OAuth.GitHub.name") }} {{ t("security_setting.configuration") }}</legend>
-
-  {% set nameForIsGitHubEnabled = "settingForm[security:passport-github:isEnabled]" %}
-  {% set isGitHubEnabled = getConfig('crowi', 'security:passport-github:isEnabled') %}
-  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
-  {% set callbackUrl = pathUtils.removeTrailingSlash(siteUrl) + '/passport/github/callback' %}
-
-  <div class="form-group">
-    <label for="{{nameForIsGitHubEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.GitHub.name") }}</label>
-    <div class="col-xs-6">
-      <div class="btn-group btn-toggle" data-toggle="buttons">
-        <label class="btn btn-default btn-rounded btn-outline {% if isGitHubEnabled %}active{% endif %}" data-active-class="primary">
-          <input name="{{nameForIsGitHubEnabled}}" value="true" type="radio"
-              {% if true === isGitHubEnabled %}checked{% endif %}> ON
-        </label>
-        <label class="btn btn-default btn-rounded btn-outline {% if !isGitHubEnabled %}active{% endif %}" data-active-class="default">
-          <input name="{{nameForIsGitHubEnabled}}" value="false" type="radio"
-              {% if !isGitHubEnabled %}checked{% endif %}> OFF
-        </label>
-      </div>
-    </div>
-  </div>
-
-
-  <div class="form-group">
-    <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
-    <div class="col-xs-6">
-        <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
-      <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-      {% if !getConfig('crowi', 'app:siteUrl') %}
-      <div class="alert alert-danger">
-        <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
-      </div>
-      {% endif %}
-    </div>
-  </div>
-
-  <fieldset id="passport-github-hide-when-disabled" {%if !isGitHubEnabled %}style="display: none;"{% endif %}>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-github:clientId]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-github:clientId]" value="{{ getConfig('crowi', 'security:passport-github:clientId') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.Use env var if empty", "OAUTH_GITHUB_CLIENT_SECRET") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-github:clientSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-github:clientSecret]" value="{{ getConfig('crowi', 'security:passport-github:clientSecret') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.Use env var if empty", "OAUTH_GITHUB_CLIENT_SECRET") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <div class="col-xs-6 col-xs-offset-3">
-        <div class="checkbox checkbox-info">
-          <input type="checkbox" id="bindByUserName-GitHub" name="settingForm[security:passport-github:isSameUsernameTreatedAsIdenticalUser]" value="1"
-              {% if getConfig('crowi', 'security:passport-github:isSameUsernameTreatedAsIdenticalUser') %}checked{% endif %} />
-          <label for="bindByUserName-GitHub">
-            {{ t("security_setting.Treat username matching as identical", "username") }}
-          </label>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.Treat username matching as identical_warn", "username") }}
-            </small>
-          </p>
-        </div>
-      </div>
-    </div>
-
-  </fieldset>
-
-  <div class="form-group" id="btn-update">
-    <div class="col-xs-offset-3 col-xs-6">
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-    </div>
-  </div>
-
-</form>
-
-{# Help Section #}
-<hr>
-
-<div style="min-height: 300px;">
-  <h4>
-    <i class="icon-question" aria-hidden="true"></i>
-    <a href="#collapseHelpForGithubOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.github") }}</a>
-  </h4>
-  <ol id="collapseHelpForGithubOauth" class="collapse">
-    <li>{{ t("security_setting.OAuth.GitHub.register_1", "https://github.com/settings/developers", "GitHub Developer Settings") }}</li>
-    <li>{{ t("security_setting.OAuth.GitHub.register_2", callbackUrl) }}</li>
-    <li>{{ t("security_setting.OAuth.GitHub.register_3") }}</li>
-  </ol>
-</div>
-
-<script>
-  $('input[name="settingForm[security:passport-github:isEnabled]"]').change(function() {
-    const isEnabled = ($(this).val() === "true");
-
-    if (isEnabled) {
-      $('#passport-github-hide-when-disabled').show(400);
-    }
-    else {
-      $('#passport-github-hide-when-disabled').hide(400);
-    }
-  });
-</script>
-

+ 0 - 120
src/server/views/admin/widget/passport/google-oauth.html

@@ -1,120 +0,0 @@
-<form action="/_api/admin/security/passport-google" method="post" class="form-horizontal passportStrategy" id="googleSetting" role="form">
-  <legend class="alert-anchor">{{ t("security_setting.OAuth.Google.name") }} {{ t("security_setting.configuration") }}</legend>
-
-  {% set nameForIsGoogleEnabled = "settingForm[security:passport-google:isEnabled]" %}
-  {% set isGoogleEnabled = getConfig('crowi', 'security:passport-google:isEnabled') | default('') %}
-  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
-  {% set callbackUrl = pathUtils.removeTrailingSlash(siteUrl) + '/passport/google/callback' %}
-
-  <div class="form-group">
-    <label for="{{nameForIsGoogleEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Google.name") }}</label>
-    <div class="col-xs-6">
-      <div class="btn-group btn-toggle" data-toggle="buttons">
-        <label class="btn btn-default btn-rounded btn-outline {% if isGoogleEnabled %}active{% endif %}" data-active-class="primary">
-          <input name="{{nameForIsGoogleEnabled}}" value="true" type="radio"
-              {% if true === isGoogleEnabled %}checked{% endif %}> ON
-        </label>
-        <label class="btn btn-default btn-rounded btn-outline {% if !isGoogleEnabled %}active{% endif %}" data-active-class="default">
-          <input name="{{nameForIsGoogleEnabled}}" value="false" type="radio"
-              {% if !isGoogleEnabled %}checked{% endif %}> OFF
-        </label>
-      </div>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
-    <div class="col-xs-6">
-        <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
-      <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-      {% if !getConfig('crowi', 'app:siteUrl') %}
-      <div class="alert alert-danger">
-        <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
-      </div>
-      {% endif %}
-    </div>
-  </div>
-
-  <fieldset id="passport-google-hide-when-disabled" {%if !isGoogleEnabled %}style="display: none;"{% endif %}>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-google:clientId]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-google:clientId]" value="{{ getConfig('crowi', 'security:passport-google:clientId') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.Use env var if empty", "OAUTH_GOOGLE_CLIENT_ID") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-google:clientSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-google:clientSecret]" value="{{ getConfig('crowi', 'security:passport-google:clientSecret') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.Use env var if empty", "OAUTH_GOOGLE_CLIENT_SECRET") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <div class="col-xs-6 col-xs-offset-3">
-        <div class="checkbox checkbox-info">
-          <input type="checkbox" id="bindByUserName-Google" name="settingForm[security:passport-google:isSameUsernameTreatedAsIdenticalUser]" value="1"
-              {% if getConfig('crowi', 'security:passport-google:isSameUsernameTreatedAsIdenticalUser') %}checked{% endif %} />
-          <label for="bindByUserName-Google">
-            {{ t("security_setting.Treat username matching as identical", "username") }}
-          </label>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.Treat username matching as identical_warn", "username") }}
-            </small>
-          </p>
-        </div>
-      </div>
-    </div>
-
-  </fieldset>
-
-  <div class="form-group" id="btn-update">
-    <div class="col-xs-offset-3 col-xs-6">
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-    </div>
-  </div>
-
-</form>
-
-{# Help Section #}
-<hr>
-
-<div style="min-height: 300px;">
-  <h4>
-    <i class="icon-question" aria-hidden="true"></i>
-    <a href="#collapseHelpForGoogleOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.google") }}</a>
-  </h4>
-  <ol id="collapseHelpForGoogleOauth" class="collapse">
-    <li>{{ t("security_setting.OAuth.Google.register_1", "https://console.cloud.google.com/apis/credentials", "Google Cloud Platform API Manager") }}</li>
-    <li>{{ t("security_setting.OAuth.Google.register_2") }}</li>
-    <li>{{ t("security_setting.OAuth.Google.register_3") }}</li>
-    <li>{{ t("security_setting.OAuth.Google.register_4", callbackUrl) }}</li>
-    <li>{{ t("security_setting.OAuth.Google.register_5") }}</li>
-  </ol>
-</div>
-
-<script>
-  $('input[name="settingForm[security:passport-google:isEnabled]"]').change(function() {
-    const isEnabled = ($(this).val() === "true");
-
-    if (isEnabled) {
-      $('#passport-google-hide-when-disabled').show(400);
-    }
-    else {
-      $('#passport-google-hide-when-disabled').hide(400);
-    }
-  });
-</script>

+ 0 - 363
src/server/views/admin/widget/passport/ldap.html

@@ -1,363 +0,0 @@
-<form action="/_api/admin/security/passport-ldap" method="post" class="form-horizontal" id="ldapSetting" role="form">
-
-  <fieldset>
-    <legend>LDAP {{ t("security_setting.configuration") }}</legend>
-
-    {% set nameForIsLdapEnabled = "settingForm[security:passport-ldap:isEnabled]" %}
-    {% set isLdapEnabled = getConfig('crowi', 'security:passport-ldap:isEnabled') %}
-    <div class="form-group">
-      <label for="{{nameForIsLdapEnabled}}" class="col-xs-3 control-label">Use LDAP</label>
-      <div class="col-xs-6">
-        <div class="btn-group btn-toggle" data-toggle="buttons">
-          <label class="btn btn-default btn-rounded btn-outline {% if isLdapEnabled %}active{% endif %}" data-active-class="primary">
-            <input name="{{nameForIsLdapEnabled}}" value="true" type="radio"
-                {% if true === isLdapEnabled %}checked{% endif %}> ON
-          </label>
-          <label class="btn btn-default btn-rounded btn-outline {% if !isLdapEnabled %}active{% endif %}" data-active-class="default">
-            <input name="{{nameForIsLdapEnabled}}" value="false" type="radio"
-                {% if !isLdapEnabled %}checked{% endif %}> OFF
-          </label>
-        </div>
-      </div>
-    </div>
-
-    <div class="passport-ldap-hide-when-disabled" {%if !isLdapEnabled %}style="display: none;"{% endif %}>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:serverUrl]" class="col-xs-3 control-label">Server URL</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text"
-              name="settingForm[security:passport-ldap:serverUrl]" value="{{ getConfig('crowi', 'security:passport-ldap:serverUrl') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.server_url_detail") }}<br>
-              {{ t("security_setting.example") }}: <code>ldaps://ldap.company.com/ou=people,dc=company,dc=com</code>
-            </small>
-          </p>
-        </div>
-      </div>
-
-      {% set nameForIsUserBind = "settingForm[security:passport-ldap:isUserBind]" %}
-      {% set isUserBind = getConfig('crowi', 'security:passport-ldap:isUserBind') %}
-      <div class="form-group">
-        <label for="{{nameForIsUserBind}}" class="col-xs-3 control-label">{{ t("security_setting.ldap.bind_mode") }}</label>
-        <div class="col-xs-6">
-          <div class="btn-group btn-toggle" data-toggle="buttons">
-            <label class="btn btn-default btn-rounded btn-outline {% if !isUserBind %}active{% endif %}" data-active-class="primary">
-              <input name="{{nameForIsUserBind}}" value="false" type="radio"
-                  {% if !isUserBind %}checked{% endif %}> {{ t("security_setting.ldap.bind_manager") }}
-            </label>
-            <label class="btn btn-default btn-rounded btn-outline {% if isUserBind %}active{% endif %}" data-active-class="primary">
-              <input name="{{nameForIsUserBind}}" value="true" type="radio"
-                  {% if isUserBind %}checked{% endif %}> {{ t("security_setting.ldap.bind_user") }}
-            </label>
-          </div>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:bindDN]" class="col-xs-3 control-label">Bind DN</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text"
-              name="settingForm[security:passport-ldap:bindDN]" value="{{ getConfig('crowi', 'security:passport-ldap:bindDN') | default('') }}">
-          <p class="help-block passport-ldap-managerbind" {% if isUserBind %}style="display: none;"{% endif %}>
-            <small>
-              {{ t("security_setting.ldap.bind_DN_manager_detail") }}<br>
-              {{ t("security_setting.example") }}1: <code>uid=admin,dc=domain,dc=com</code><br>
-              {{ t("security_setting.example") }}2: <code>admin@domain.com</code>
-            </small>
-          </p>
-          <p class="help-block passport-ldap-userbind" {% if !isUserBind %}style="display: none;"{% endif %}>
-            <small>
-              {{ t("security_setting.ldap.bind_DN_user_detail1") }}<br>
-              {{ t("security_setting.ldap.bind_DN_user_detail2") }}<br>
-              {{ t("security_setting.example") }}1: <code>uid={% raw %}{{username}}{% endraw %},dc=domain,dc=com</code><br>
-              {{ t("security_setting.example") }}2: <code>{% raw %}{{username}}{% endraw %}@domain.com</code>
-            </small>
-          </p>
-          </div>
-      </div>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:bindDNPassword]" class="col-xs-3 control-label">{{ t("security_setting.ldap.bind_DN_password") }}</label>
-        <div class="col-xs-6">
-          <input class="form-control passport-ldap-managerbind" type="password" {% if isUserBind %}style="display: none;"{% endif %}
-              name="settingForm[security:passport-ldap:bindDNPassword]" value="{{ getConfig('crowi', 'security:passport-ldap:bindDNPassword') | default('') }}">
-          <p class="help-block passport-ldap-managerbind">
-            <small>
-              {{ t("security_setting.ldap.bind_DN_password_manager_detail") }}
-            </small>
-          </p>
-          <p class="help-block passport-ldap-userbind" {% if !isUserBind %}style="display: none;"{% endif %}>
-            <small>
-              {{ t("security_setting.ldap.bind_DN_password_user_detail") }}
-            </small>
-          </p>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:searchFilter]" class="col-xs-3 control-label">{{ t("security_setting.ldap.search_filter") }}</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text" placeholder="Default: (uid={% raw %}{{username}}{% endraw %})"
-              name="settingForm[security:passport-ldap:searchFilter]" value="{{ getConfig('crowi', 'security:passport-ldap:searchFilter') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.search_filter_detail1") }}<br>
-              {{ t("security_setting.ldap.search_filter_detail2") }}<br>
-              {{ t("security_setting.ldap.search_filter_detail3") }}
-            </small>
-          </p>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.example") }}1 - {{ t("security_setting.ldap.search_filter_example1") }}: <code>(|(uid={% raw %}{{username}}{% endraw %})(mail={% raw %}{{username}}{% endraw %}))</code><br>
-              {{ t("security_setting.example") }}2 - {{ t("security_setting.ldap.search_filter_example2") }}: <code>(sAMAccountName={% raw %}{{username}}{% endraw %})</code>
-            </small>
-          </p>
-        </div>
-      </div>
-
-      <h4>Attribute Mapping ({{ t("security_setting.optional") }})</h4>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:attrMapUsername]" class="col-xs-3 control-label">username</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text" placeholder="Default: uid"
-              name="settingForm[security:passport-ldap:attrMapUsername]" value="{{ getConfig('crowi', 'security:passport-ldap:attrMapUsername') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.username_detail") }}
-            </small>
-          </p>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <div class="col-xs-6 col-xs-offset-3">
-          <div class="checkbox checkbox-info">
-            <input type="checkbox" id="cbSameUsernameTreatedAsIdenticalUser" name="settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]" value="1"
-                {% if getConfig('crowi', 'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser') %}checked{% endif %} />
-            <label for="cbSameUsernameTreatedAsIdenticalUser">
-              {{ t("security_setting.Treat username matching as identical", "username") }}
-            </label>
-            <p class="help-block">
-              <small>
-                {{ t("security_setting.Treat username matching as identical_warn", "username") }}
-              </small>
-            </p>
-          </div>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:attrMapMail]" class="col-xs-3 control-label">Mail</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text" placeholder="Default: mail"
-              name="settingForm[security:passport-ldap:attrMapMail]" value="{{ getConfig('crowi', 'security:passport-ldap:attrMapMail') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.mail_detail") }}
-            </small>
-          </p>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:attrMapName]" class="col-xs-3 control-label">Name</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text"
-              name="settingForm[security:passport-ldap:attrMapName]" value="{{ getConfig('crowi', 'security:passport-ldap:attrMapName') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.name_detail") }}
-            </small>
-          </p>
-        </div>
-      </div>
-
-      <h4>{{ t("security_setting.ldap.group_search_filter") }} ({{ t("security_setting.optional") }})</h4>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:groupSearchBase]" class="col-xs-3 control-label">{{ t("security_setting.ldap.group_search_base_DN") }}</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text"
-              name="settingForm[security:passport-ldap:groupSearchBase]" value="{{ getConfig('crowi', 'security:passport-ldap:groupSearchBase') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.group_search_base_DN_detail") }}<br>
-              {{ t("security_setting.example") }}: <code>ou=groups,dc=domain,dc=com</code>
-            </small>
-          </p>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:groupSearchFilter]" class="col-xs-3 control-label">{{ t("security_setting.ldap.group_search_filter") }}</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text"
-              name="settingForm[security:passport-ldap:groupSearchFilter]" value="{{ getConfig('crowi', 'security:passport-ldap:groupSearchFilter') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.group_search_filter_detail1") }}<br>
-              {{ t("security_setting.ldap.group_search_filter_detail2") }}<br>
-              {{ t("security_setting.ldap.group_search_filter_detail3") }}
-            </small>
-          </p>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.example") }}: {{ t("security_setting.ldap.group_search_filter_detail4") }}
-            </small>
-          </p>
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="settingForm[security:passport-ldap:groupSearchFilter]" class="col-xs-3 control-label">{{ t("security_setting.ldap.group_search_user_DN_property") }}</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="text" placeholder="Default: uid"
-              name="settingForm[security:passport-ldap:groupDnProperty]" value="{{ getConfig('crowi', 'security:passport-ldap:groupDnProperty') | default('') }}">
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.ldap.group_search_user_DN_property_detail") }}
-            </small>
-          </p>
-        </div>
-      </div>
-
-    </div><!-- /.passport-ldap-configurations -->
-
-    <div class="form-group">
-      <div class="col-xs-offset-3 col-xs-6">
-        <button type="submit" class="btn btn-primary">{# the first element is the default button to submit #}
-          {{ t('Update') }}
-        </button>
-        <button type="button"
-            class="btn btn-default passport-ldap-hide-when-disabled"
-            data-target="#test-ldap-account" data-toggle="modal"
-            {%if !isLdapEnabled %}style="display: none;"{% endif %}>
-
-          {{ t("security_setting.ldap.test_config") }}
-        </button>
-      </div>
-    </div>
-  </fieldset>
-  <input type="hidden" name="_csrf" value="{{ csrf() }}">
-
-  <script>
-    // switch display according to on / off of radio buttons
-    $('input[name="{{nameForIsLdapEnabled}}"]:radio').change(function() {
-      const isEnabled = ($(this).val() === "true");
-
-      if (isEnabled) {
-        $('.passport-ldap-hide-when-disabled').show(400);
-      }
-      else {
-        $('.passport-ldap-hide-when-disabled').hide(400);
-      }
-    });
-
-    // switch display according to on / off of radio buttons
-    $('input[name="{{nameForIsUserBind}}"]:radio').change(function() {
-      const isUserBind = ($(this).val() === "true");
-
-      if (isUserBind) {
-        $('input.passport-ldap-managerbind').hide();
-        $('.help-block.passport-ldap-managerbind').hide();
-        $('.help-block.passport-ldap-userbind').show();
-      }
-      else {
-        $('input.passport-ldap-managerbind').show();
-        $('.help-block.passport-ldap-managerbind').show();
-        $('.help-block.passport-ldap-userbind').hide();
-      }
-    });
-
-    // store which button is clicked when submit
-    var submittedButton;
-    $('button[type="submit"]').click(function() {
-      submittedButton = $(this);
-    });
-    $('#ldapSetting, #ldapTest').each(function() {
-      $(this).submit(function()
-      {
-        function showMessage(formId, msg, status) {
-          $('#' + formId + ' .alert').remove();
-
-          if (!status) {
-            status = 'success';
-          }
-          var $message = $('<p class="alert"></p>');
-          $message.addClass('alert-' + status);
-          $message.html(msg.replace(/\n/g, '<br>'));
-          $message.insertAfter('#' + formId + ' legend');
-
-          if (status == 'success') {
-            setTimeout(function()
-            {
-              $message.fadeOut({
-                complete: function() {
-                  $message.remove();
-                }
-              });
-            }, 5000);
-          }
-        }
-
-        var $form = $(this);
-        var $id = $form.attr('id');
-        var $button = submittedButton;
-        var $action = $button.attr('formaction') || $form.attr('action');
-        $button.attr('disabled', 'disabled');
-        var jqxhr = $.post($action, $form.serialize(), function(data)
-        {
-          if (data.status) {
-            const message = data.message || '更新しました';
-            showMessage($id, message);
-          } else {
-            showMessage($id, data.message, 'danger');
-          }
-        })
-        .fail(function() {
-          showMessage($id, 'エラーが発生しました', 'danger');
-        })
-        .always(function() {
-          $button.prop('disabled', false);
-        });
-        return false;
-      });
-    });
-    </script>
-
-</form>
-
-<div class="modal test-ldap-account" id="test-ldap-account">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <div class="modal-header">
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <div class="modal-title">{{ t('Test LDAP Account') }}</div>
-      </div>
-
-      <div class="modal-body">
-
-        {% include '../../../widget/passport/ldap-association-tester.html' with { showLog: true } %}
-
-      </div><!-- /.modal-body -->
-
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-
-  <script>
-    /**
-     * associate (submit the form)
-     */
-    function associateLdap() {
-      var $form = $('#formLdapAssociationContainer > form');
-      var $action = '/me/external-accounts/associateLdap';
-      $form.attr('action', $action);
-      $form.submit();
-    }
-  </script>
-
-</div><!-- /.modal -->

+ 0 - 84
src/server/views/admin/widget/passport/local.html

@@ -1,84 +0,0 @@
-<form action="/_api/admin/security/passport-local" method="post" class="form-horizontal passportStrategy" id="localSetting" role="form"
-    {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
-  <legend class="alert-anchor">{{ t("security_setting.Local.name") }} {{ t("security_setting.configuration") }}</legend>
-
-  {% set nameForIsLocalEnabled = "settingForm[security:passport-local:isEnabled]" %}
-  {% set isLocalEnabled = getConfig('crowi', 'security:passport-local:isEnabled') %}
-  {% set useOnlyEnvVars = getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions') %}
-
-  {% if useOnlyEnvVars %}
-    <p class="alert alert-info">
-      {{ t("security_setting.Local.note for the only env option", "LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS") }}
-    </p>
-  {% endif %}
-
-  <div class="form-group">
-    <label for="{{nameForIsLocalEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.Local.name") }}</label>
-    <div class="col-xs-6">
-      <div class="btn-group btn-toggle {% if useOnlyEnvVars %}btn-group-disabled{% endif %}" data-toggle="buttons">
-        <label class="btn btn-default btn-rounded btn-outline {% if isLocalEnabled %}active{% endif %}" data-active-class="primary">
-          <input name="{{nameForIsLocalEnabled}}"
-                 value="true"
-                 type="radio"
-                 {% if true === isLocalEnabled %}checked{% endif %}
-                 {% if useOnlyEnvVars %}readonly{% endif %}> ON
-        </label>
-        <label class="btn btn-default btn-rounded btn-outline {% if !isLocalEnabled %}active{% endif %}" data-active-class="default">
-          <input name="{{nameForIsLocalEnabled}}"
-                 value="false"
-                 type="radio"
-                 {% if !isLocalEnabled %}checked{% endif %}
-                 {% if useOnlyEnvVars %}readonly{% endif %}> OFF
-        </label>
-      </div>
-    </div>
-  </div>
-
-
-  <fieldset id="passport-local-hide-when-disabled" {%if !isLocalEnabled %}style="display: none;"{% endif %}>
-
-    <div class="form-group">
-      <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Register limitation') }}</label>
-      <div class="col-xs-9 col-lg-6">
-        <select class="form-control selectpicker" name="settingForm[security:registrationMode]" value="{{ getConfig('crowi', 'security:registrationMode') }}">
-          {% for modeValue, modeLabel in consts.registrationMode %}
-          <option value="{{ t(modeValue) }}" {% if modeValue == getConfig('crowi', 'security:registrationMode') %}selected{% endif %} >{{ t(modeLabel) }}</option>
-          {% endfor %}
-        </select>
-        <p class="help-block small">{{ t('security_setting.Register limitation desc') }}</p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:registrationWhiteList]" class="col-xs-3 control-label">{{ t('The whitelist of registration permission E-mail address') }}</label>
-      <div class="col-xs-9 col-lg-6">
-        <textarea class="form-control" type="textarea" name="settingForm[security:registrationWhiteList]" placeholder="{{ t('security_setting.example') }}: @growi.org">{{ getConfig('crowi', 'security:registrationWhiteList') | join('&#13') | raw }}</textarea>
-        <p class="help-block small">{{ t("security_setting.restrict_emails") }}{{ t("security_setting.for_instance") }}<code>@growi.org</code>{{ t("security_setting.only_those") }}<br>
-        {{ t("security_setting.insert_single") }}</p>
-      </div>
-    </div>
-
-  </fieldset>
-
-  <div class="form-group" id="btn-update">
-    <div class="col-xs-offset-3 col-xs-6">
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-    </div>
-  </div>
-
-</form>
-
-<script>
-  $('input[name="settingForm[security:passport-local:isEnabled]"]').change(function() {
-    const isEnabled = ($(this).val() === "true");
-
-    if (isEnabled) {
-      $('#passport-local-hide-when-disabled').show(400);
-    }
-    else {
-      $('#passport-local-hide-when-disabled').hide(400);
-    }
-  });
-</script>
-

+ 0 - 218
src/server/views/admin/widget/passport/oidc.html

@@ -1,218 +0,0 @@
-<form action="/_api/admin/security/passport-oidc" method="post" class="form-horizontal passportStrategy" id="oidcSetting" role="form">
-  <legend class="alert-anchor">{{ t("security_setting.OAuth.OIDC.name") }} {{ t("security_setting.configuration") }}</legend>
-
-  {% set nameForIsOIDCEnabled = "settingForm[security:passport-oidc:isEnabled]" %}
-  {% set isOidcEnabled = getConfig('crowi', 'security:passport-oidc:isEnabled') %}
-  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
-  {% set callbackUrl = pathUtils.removeTrailingSlash(siteUrl) + '/passport/oidc/callback' %}
-
-  <div class="form-group">
-    <label for="{{nameForIsOIDCEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.OIDC.name") }}</label>
-    <div class="col-xs-6">
-      <div class="btn-group btn-toggle" data-toggle="buttons">
-        <label class="btn btn-default btn-rounded btn-outline {% if isOidcEnabled %}active{% endif %}" data-active-class="primary">
-          <input name="{{nameForIsOIDCEnabled}}" value="true" type="radio"
-              {% if true === isOidcEnabled %}checked{% endif %}> ON
-        </label>
-        <label class="btn btn-default btn-rounded btn-outline {% if !isOidcEnabled %}active{% endif %}" data-active-class="default">
-          <input name="{{nameForIsOIDCEnabled}}" value="false" type="radio"
-              {% if !isOidcEnabled %}checked{% endif %}> OFF
-        </label>
-      </div>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
-    <div class="col-xs-6">
-      <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
-      <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-      {% if !getConfig('crowi', 'app:siteUrl') %}
-      <div class="alert alert-danger">
-        <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
-      </div>
-      {% endif %}
-    </div>
-  </div>
-
-  <fieldset id="passport-oidc-hide-when-disabled" {%if !isOidcEnabled %}style="display: none;"{% endif %}>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:providerName]" class="col-xs-3 control-label">{{ t("security_setting.providerName") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:providerName]" value="{{ getConfig('crowi', 'security:passport-oidc:providerName') | default('') }}">
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:issuerHost]" class="col-xs-3 control-label">{{ t("security_setting.issuerHost") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:issuerHost]" value="{{ getConfig('crowi', 'security:passport-oidc:issuerHost') | default('') }}">
-        <p class="help-block">
-          <small>
-                {{ t("security_setting.Use env var if empty", "OAUTH_OIDC_ISSUER_HOST") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:clientId]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:clientId]" value="{{ getConfig('crowi', 'security:passport-oidc:clientId') | default('') }}">
-        <p class="help-block">
-          <small>
-             {{ t("security_setting.Use env var if empty", "OAUTH_OIDC_CLIENT_ID") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:clientSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:clientSecret]" value="{{ getConfig('crowi', 'security:passport-oidc:clientSecret') | default('') }}">
-        <p class="help-block">
-          <small>
-             {{ t("security_setting.Use env var if empty", "OAUTH_OIDC_CLIENT_SECRET") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <h4>Attribute Mapping ({{ t("security_setting.optional") }})</h4>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:attrMapId]" class="col-xs-3 control-label">Identifier</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:attrMapId]" value="{{ getConfig('crowi', 'security:passport-oidc:attrMapId') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.OAuth.OIDC.id_detail") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:attrMapUserName]" class="col-xs-3 control-label">Username</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:attrMapUserName]" value="{{ getConfig('crowi', 'security:passport-oidc:attrMapUserName') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.OAuth.OIDC.username_detail") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:attrMapName]" class="col-xs-3 control-label">Name</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:attrMapName]" value="{{ getConfig('crowi', 'security:passport-oidc:attrMapName') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.OAuth.OIDC.name_detail") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-oidc:attrMapMail]" class="col-xs-3 control-label">Mail</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:attrMapMail]" value="{{ getConfig('crowi', 'security:passport-oidc:attrMapMail') | default('') }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.OAuth.OIDC.mapping_detail", t("Email")) }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
-      <div class="col-xs-6">
-          <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
-        <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-        {% if !getConfig('crowi', 'app:siteUrl') %}
-        <div class="alert alert-danger">
-          <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
-        </div>
-        {% endif %}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <div class="col-xs-6 col-xs-offset-3">
-        <div class="checkbox checkbox-info">
-          <input type="checkbox" id="bindByUserName-oidc" name="settingForm[security:passport-oidc:isSameUsernameTreatedAsIdenticalUser]" value="1"
-              {% if getConfig('crowi', 'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser') %}checked{% endif %} />
-          <label for="bindByUserName-oidc">
-            {{ t("security_setting.Treat username matching as identical", "username") }}
-          </label>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.Treat username matching as identical_warn", "username") }}
-            </small>
-          </p>
-        </div>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <div class="col-xs-6 col-xs-offset-3">
-        <div class="checkbox checkbox-info">
-          <input type="checkbox" id="bindByEmail-oidc" name="settingForm[security:passport-oidc:isSameEmailTreatedAsIdenticalUser]" value="1"
-              {% if getConfig('crowi', 'security:passport-oidc:isSameEmailTreatedAsIdenticalUser') %}checked{% endif %} />
-          <label for="bindByEmail-oidc">
-            {{ t("security_setting.Treat email matching as identical", "email") }}
-          </label>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.Treat email matching as identical_warn", "email") }}
-            </small>
-          </p>
-        </div>
-      </div>
-    </div>
-
-  </fieldset>
-
-  <div class="form-group" id="btn-update">
-    <div class="col-xs-offset-3 col-xs-6">
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-    </div>
-  </div>
-
-</form>
-
-{# Help Section #}
-<hr>
-
-<div style="min-height: 300px;">
-  <h4>
-    <i class="icon-question" aria-hidden="true"></i>
-    <a href="#collapseHelpForOidcOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.oidc") }}</a>
-  </h4>
-  <ol id="collapseHelpForOidcOauth" class="collapse">
-    <li>{{ t("security_setting.OAuth.OIDC.register_1") }}</li>
-    <li>{{ t("security_setting.OAuth.OIDC.register_2", callbackUrl) }}</li>
-    <li>{{ t("security_setting.OAuth.OIDC.register_3") }}</li>
-  </ol>
-</div>
-
-<script>
-  $('input[name="settingForm[security:passport-oidc:isEnabled]"]').change(function() {
-      const isEnabled = ($(this).val() === "true");
-
-      if (isEnabled) {
-        $('#passport-oidc-hide-when-disabled').show(400);
-      }
-      else {
-        $('#passport-oidc-hide-when-disabled').hide(400);
-      }
-    });
-</script>
-

+ 0 - 456
src/server/views/admin/widget/passport/saml.html

@@ -1,456 +0,0 @@
-<form action="/_api/admin/security/passport-saml" method="post" class="form-horizontal passportStrategy" id="samlSetting" role="form">
-  <legend class="alert-anchor">{{ t("security_setting.SAML.name") }} {{ t("security_setting.configuration") }}</legend>
-
-  {% set nameForIsSamlEnabled = "settingForm[security:passport-saml:isEnabled]" %}
-  {% set isSamlEnabled  = getConfig('crowi', 'security:passport-saml:isEnabled') %}
-  {% set useOnlyEnvVars = getConfig('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions') %}
-  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
-  {% set callbackUrl = pathUtils.removeTrailingSlash(siteUrl) + '/passport/saml/callback' %}
-
-  {% if useOnlyEnvVars %}
-    <p class="alert alert-info">
-      {{ t("security_setting.SAML.note for the only env option", "SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS") }}
-    </p>
-  {% endif %}
-
-  <div class="form-group">
-    <label class="col-xs-3 control-label">{{ t("security_setting.SAML.name") }}</label>
-    <div class="col-xs-6">
-      <div class="btn-group btn-toggle {% if useOnlyEnvVars %}btn-group-disabled{% endif %}" data-toggle="buttons">
-        <label class="btn btn-default btn-rounded btn-outline {% if isSamlEnabled %}active{% endif %}" data-active-class="primary">
-          <input name="{{nameForIsSamlEnabled}}"
-                 value="true"
-                 type="radio"
-                 {% if true === isSamlEnabled %}checked{% endif %}
-                 {% if useOnlyEnvVars %}readonly{% endif %}> ON
-        </label>
-        <label class="btn btn-default btn-rounded btn-outline {% if !isSamlEnabled %}active{% endif %}" data-active-class="default">
-          <input name="{{nameForIsSamlEnabled}}"
-                 value="false"
-                 type="radio"
-                 {% if !isSamlEnabled %}checked{% endif %}
-                 {% if useOnlyEnvVars %}readonly{% endif %}> OFF
-        </label>
-      </div>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
-    <div class="col-xs-6">
-      <input class="form-control"
-             type="text"
-             value="{{ callbackUrl }}"
-             readonly>
-      <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'SAML Identity') }}</p>
-      {% if !getConfig('crowi', 'app:siteUrl') %}
-      <div class="alert alert-danger">
-        <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
-      </div>
-      {% endif %}
-    </div>
-  </div>
-
-  <fieldset id="passport-saml-hide-when-disabled" {%if !isSamlEnabled %}style="display: none;"{% endif %}>
-
-    {% set missingMandatoryConfigKeys = getSamlMissingMandatoryConfigKeys() %}
-    {% if missingMandatoryConfigKeys.length !== 0 %}
-    <div class="alert alert-danger">
-      {{ t("security_setting.missing mandatory configs") }}
-      <ul>
-        {% for missingMandatoryConfigKey in missingMandatoryConfigKeys %}
-        <li>{{ t("security_setting.form_item_name." + missingMandatoryConfigKey) }}</li>
-        {% endfor %}
-      </ul>
-    </div>
-    {% endif %}
-
-    <h4>Basic Settings</h4>
-    <table class="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
-      <colgroup>
-        <col class="item-name">
-        <col class="from-db">
-        <col class="from-env-vars">
-      </colgroup>
-      <thead>
-        <tr><th></th><th>Database</th><th>Environment variables</th></tr>
-      </thead>
-      <tbody>
-        <tr>
-          <th>{{ t("security_setting.form_item_name.security:passport-saml:entryPoint") }}</th>
-          <td>
-            <input class="form-control"
-                   type="text"
-                   name="settingForm[security:passport-saml:entryPoint]"
-                   value="{{ getConfigFromDB('crowi', 'security:passport-saml:entryPoint') || '' }}"
-                   {% if useOnlyEnvVars %}readonly{% endif %}>
-          </td>
-          <td>
-            <input class="form-control"
-                   type="text"
-                   value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:entryPoint') || '' }}"
-                   readonly>
-            <p class="help-block">
-              <small>
-                {{ t("security_setting.SAML.Use env var if empty", "SAML_ENTRY_POINT") }}
-              </small>
-            </p>
-          </td>
-        </tr>
-        <tr>
-          <th>{{ t("security_setting.form_item_name.security:passport-saml:issuer") }}</th>
-          <td>
-            <input class="form-control"
-                   type="text"
-                   name="settingForm[security:passport-saml:issuer]"
-                   value="{{ getConfigFromDB('crowi', 'security:passport-saml:issuer') || '' }}"
-                   {% if useOnlyEnvVars %}readonly{% endif %}>
-          </td>
-          <td>
-            <input class="form-control"
-                   type="text"
-                   value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:issuer') || '' }}"
-                   readonly>
-            <p class="help-block">
-              <small>
-                {{ t("security_setting.SAML.Use env var if empty", "SAML_ISSUER") }}
-              </small>
-            </p>
-          </td>
-        </tr>
-        <tr>
-          <th>{{ t("security_setting.form_item_name.security:passport-saml:cert") }}</th>
-          <td>
-            <textarea class="form-control input-sm"
-                      type="text"
-                      rows="5"
-                      name="settingForm[security:passport-saml:cert]"
-                      {% if useOnlyEnvVars %}readonly{% endif %}
-            >{{ getConfigFromDB('crowi', 'security:passport-saml:cert') || '' }}</textarea>
-            <p class="help-block">
-              <small>
-                {{ t("security_setting.SAML.cert_detail") }}
-              </small>
-            </p>
-            <p>
-              <small>
-                e.g.
-                <pre>-----BEGIN CERTIFICATE-----
-MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
-UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
-...
-crmVwBzbloUO2l6k1ibwD2WVwpdxMKIF5z58HfKAvxZAzCHE7kMEZr1ge30WRXQA
-pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
------END CERTIFICATE-----</pre>
-              </small>
-            </p>
-          </td>
-          <td>
-            <textarea class="form-control input-sm"
-                      type="text"
-                      rows="5"
-                      readonly
-            >{{ getConfigFromEnvVars('crowi', 'security:passport-saml:cert') || '' }}</textarea>
-            <p class="help-block">
-              <small>
-                {{ t("security_setting.SAML.Use env var if empty", "SAML_CERT") }}
-              </small>
-            </p>
-          </td>
-        </tr>
-      </tbody>
-    </table>
-
-    <h4>Attribute Mapping</h4>
-
-    <table class="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
-      <colgroup>
-        <col class="item-name">
-        <col class="from-db">
-        <col class="from-env-vars">
-      </colgroup>
-      <thead>
-        <tr><th></th><th>Database</th><th>Environment variables</th></tr>
-      </thead>
-      <tbody>
-      <tr>
-        <th>{{ t("security_setting.form_item_name.security:passport-saml:attrMapId") }}</th>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 name="settingForm[security:passport-saml:attrMapId]"
-                 value="{{ getConfigFromDB('crowi', 'security:passport-saml:attrMapId') || '' }}"
-                 {% if useOnlyEnvVars %}readonly{% endif %}>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.id_detail") }}
-            </small>
-          </p>
-        </td>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapId') || '' }}"
-                 readonly>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.Use env var if empty", "SAML_ATTR_MAPPING_ID") }}
-            </small>
-          </p>
-        </td>
-      </tr>
-      <tr>
-        <th>{{ t("security_setting.form_item_name.security:passport-saml:attrMapUsername") }}</th>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 name="settingForm[security:passport-saml:attrMapUsername]"
-                 value="{{ getConfigFromDB('crowi', 'security:passport-saml:attrMapUsername') || '' }}"
-                 {% if useOnlyEnvVars %}readonly{% endif %}>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.username_detail") }}
-            </small>
-          </p>
-        </td>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapUsername') || '' }}"
-                 readonly>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.Use env var if empty", "SAML_ATTR_MAPPING_USERNAME") }}
-            </small>
-          </p>
-        </td>
-      </tr>
-      <tr>
-        <th>{{ t("security_setting.form_item_name.security:passport-saml:attrMapMail") }}</th>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 name="settingForm[security:passport-saml:attrMapMail]"
-                 value="{{ getConfigFromDB('crowi', 'security:passport-saml:attrMapMail') || '' }}"
-                 {% if useOnlyEnvVars %}readonly{% endif %}>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.mapping_detail", t("Email")) }}
-            </small>
-        </td>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapMail') || '' }}"
-                 readonly>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.Use env var if empty", "SAML_ATTR_MAPPING_MAIL") }}
-            </small>
-          </p>
-        </td>
-      </tr>
-      <tr>
-        <th>{{ t("security_setting.form_item_name.security:passport-saml:attrMapFirstName") }}</th>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 name="settingForm[security:passport-saml:attrMapFirstName]"
-                 value="{{ getConfigFromDB('crowi', 'security:passport-saml:attrMapFirstName') || '' }}"
-                 {% if useOnlyEnvVars %}readonly{% endif %}>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.mapping_detail", t("security_setting.form_item_name.security:passport-saml:attrMapFirstName")) }}
-            </small>
-          </p>
-        </td>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapFirstName') || '' }}"
-                 readonly>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.Use env var if empty", "SAML_ATTR_MAPPING_FIRST_NAME") }}<br>
-              {{ t("security_setting.Use default if both are empty", "firstName") }}
-            </small>
-          </p>
-        </td>
-      </tr>
-      <tr>
-        <th>{{ t("security_setting.form_item_name.security:passport-saml:attrMapLastName") }}</th>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 name="settingForm[security:passport-saml:attrMapLastName]"
-                 value="{{ getConfigFromDB('crowi', 'security:passport-saml:attrMapLastName') || '' }}"
-                 {% if useOnlyEnvVars %}readonly{% endif %}>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.mapping_detail", t("security_setting.form_item_name.security:passport-saml:attrMapLastName")) }}
-            </small>
-          </p>
-        </td>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapLastName') || '' }}"
-                 readonly>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.Use env var if empty", "SAML_ATTR_MAPPING_LAST_NAME") }}<br>
-              {{ t("security_setting.Use default if both are empty", "lastName") }}
-            </small>
-          </p>
-        </td>
-      </tr>
-      </tbody>
-    </table>
-
-    <h4>Attribute Mapping Options</h4>
-
-    <div class="form-group">
-      <div class="col-xs-offset-1">
-        <div class="checkbox checkbox-info">
-          <input id="bindByUserName-SAML"
-                 type="checkbox"
-                 name="settingForm[security:passport-saml:isSameUsernameTreatedAsIdenticalUser]"
-                 value="1"
-                 {% if getConfig('crowi', 'security:passport-saml:isSameUsernameTreatedAsIdenticalUser') %}checked{% endif %} />
-          <label for="bindByUserName-SAML">
-            {{ t("security_setting.Treat username matching as identical", "username") }}
-          </label>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.Treat username matching as identical_warn", "username") }}
-            </small>
-          </p>
-        </div>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <div class="col-xs-offset-1">
-        <div class="checkbox checkbox-info">
-          <input id="bindByEmail-SAML"
-                 type="checkbox"
-                 name="settingForm[security:passport-saml:isSameEmailTreatedAsIdenticalUser]"
-                 value="1"
-                 {% if getConfig('crowi', 'security:passport-saml:isSameEmailTreatedAsIdenticalUser') %}checked{% endif %} />
-          <label for="bindByEmail-SAML">
-            {{ t("security_setting.Treat email matching as identical", "email") }}
-          </label>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.Treat email matching as identical_warn", "email") }}
-            </small>
-          </p>
-        </div>
-      </div>
-    </div>
-
-    <h4>Attribute-based Login Control</h4>
-
-    <p class="help-block">
-      <small>
-        {{ t("security_setting.SAML.attr_based_login_control_detail") }}
-      </small>
-    </p>
-
-    <table class="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
-      <colgroup>
-        <col class="item-name">
-        <col class="from-db">
-        <col class="from-env-vars">
-      </colgroup>
-      <thead>
-        <tr><th></th><th>Database</th><th>Environment variables</th></tr>
-      </thead>
-      <tbody>
-      <tr>
-        <th>
-          {{ t("security_setting.form_item_name.security:passport-saml:ABLCRule") }}
-        </th>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 name="settingForm[security:passport-saml:ABLCRule]"
-                 value="{{ getConfigFromDB('crowi', 'security:passport-saml:ABLCRule') || '' }}"
-                 {% if useOnlyEnvVars %}readonly{% endif %}>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.attr_based_login_control_rule_detail") }}<br>
-              {{ t("security_setting.SAML.attr_based_login_control_rule_example") }}
-            </small>
-          </p>
-        </td>
-        <td>
-          <input class="form-control"
-                 type="text"
-                 value="{{ getConfigFromEnvVars('crowi', 'security:passport-saml:ABLCRule') || '' }}"
-                 readonly>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.SAML.Use env var if empty", "SAML_ABLC_RULE") }}
-            </small>
-          </p>
-        </td>
-      </tr>
-      </tbody>
-    </table>
-
-  </fieldset>
-
-  <div class="form-group" id="btn-update">
-    <div class="col-xs-offset-3 col-xs-6">
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-    </div>
-  </div>
-
-</form>
-
-<script>
-  $('.btn-group-disabled').on('click', '.btn', function() {
-    return false;
-  });
-
-  $('input[name="settingForm[security:passport-saml:isEnabled]"]').change(function() {
-    const isEnabled = ($(this).val() === "true");
-
-    if (isEnabled) {
-      $('#passport-saml-hide-when-disabled').show(400);
-    }
-    else {
-      $('#passport-saml-hide-when-disabled').hide(400);
-    }
-  });
-
-
-  /**
-   * The following script sets the class name 'unused' to the cell in from-env-vars column
-   * when the value of the corresponding cell from the database is not empty.
-   * It is used to indicate that the system does not use a value from the environment variables by setting a css style.
-   * This behavior is disabled when the system is in the use-only-env-vars mode.
-   */
-  $('.settings-table:not(.use-only-env-vars) tbody tr').each(function(_, element) {
-    const inputElemFromDB      = $('td:nth-of-type(1) input[type="text"], td:nth-of-type(1) textarea', element);
-    const inputElemFromEnvVars = $('td:nth-of-type(2) input[type="text"], td:nth-of-type(2) textarea', element);
-
-    // initialize
-    addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars);
-
-    // set keyup event handler
-    inputElemFromDB.keyup(function () { addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars) });
-  });
-
-  function addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars) {
-    if (inputElemFromDB.val() === '') {
-      inputElemFromEnvVars.parent().removeClass('unused');
-    }
-    else {
-      inputElemFromEnvVars.parent().addClass('unused');
-    }
-  };
-</script>
-

+ 0 - 120
src/server/views/admin/widget/passport/twitter.html

@@ -1,120 +0,0 @@
-<form action="/_api/admin/security/passport-twitter" method="post" class="form-horizontal passportStrategy" id="twitterSetting" role="form">
-  <legend class="alert-anchor">{{ t("security_setting.OAuth.Twitter.name") }} {{ t("security_setting.configuration") }}</legend>
-
-  {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
-  {% set isTwitterEnabled = getConfig('crowi', 'security:passport-twitter:isEnabled') %}
-  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
-  {% set callbackUrl = pathUtils.removeTrailingSlash(siteUrl) + '/passport/twitter/callback' %}
-
-  <div class="form-group">
-    <label for="{{nameForIsTwitterEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Twitter.name") }}</label>
-    <div class="col-xs-6">
-      <div class="btn-group btn-toggle" data-toggle="buttons">
-        <label class="btn btn-default btn-rounded btn-outline {% if isTwitterEnabled %}active{% endif %}" data-active-class="primary">
-          <input name="{{nameForIsTwitterEnabled}}" value="true" type="radio"
-              {% if true === isTwitterEnabled %}checked{% endif %}> ON
-        </label>
-        <label class="btn btn-default btn-rounded btn-outline {% if !isTwitterEnabled %}active{% endif %}" data-active-class="default">
-          <input name="{{nameForIsTwitterEnabled}}" value="false" type="radio"
-              {% if !isTwitterEnabled %}checked{% endif %}> OFF
-        </label>
-      </div>
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
-    <div class="col-xs-6">
-      <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
-      <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-      {% if !getConfig('crowi', 'app:siteUrl') %}
-      <div class="alert alert-danger">
-        <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
-      </div>
-      {% endif %}
-    </div>
-  </div>
-
-  <fieldset id="passport-twitter-hide-when-disabled" {%if !isTwitterEnabled %}style="display: none;"{% endif %}>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-twitter:consumerKey]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-twitter:consumerKey]" value="{{ getConfig('crowi', 'security:passport-twitter:consumerKey') | default('') }}">
-        <p class="help-block">
-          <small>
-                {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CONSUMER_KEY") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="settingForm[security:passport-twitter:consumerSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
-      <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-twitter:consumerSecret]" value="{{ getConfig('crowi', 'security:passport-twitter:consumerSecret') | default('') }}">
-        <p class="help-block">
-          <small>
-             {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CONSUMER_SECRET") }}
-          </small>
-        </p>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <div class="col-xs-6 col-xs-offset-3">
-        <div class="checkbox checkbox-info">
-          <input type="checkbox" id="bindByUserName-Twitter" name="settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]" value="1"
-              {% if getConfig('crowi', 'security:passport-twitter:isSameUsernameTreatedAsIdenticalUser') %}checked{% endif %} />
-          <label for="bindByUserName-Twitter">
-            {{ t("security_setting.Treat username matching as identical", "username") }}
-          </label>
-          <p class="help-block">
-            <small>
-              {{ t("security_setting.Treat username matching as identical_warn", "username") }}
-            </small>
-          </p>
-        </div>
-      </div>
-    </div>
-
-  </fieldset>
-
-  <div class="form-group" id="btn-update">
-    <div class="col-xs-offset-3 col-xs-6">
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-    </div>
-  </div>
-
-</form>
-
-{# Help Section #}
-<hr>
-
-<div style="min-height: 300px;">
-  <h4>
-    <i class="icon-question" aria-hidden="true"></i>
-    <a href="#collapseHelpForTwitterOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.twitter") }}</a>
-  </h4>
-  <ol id="collapseHelpForTwitterOauth" class="collapse">
-    <li>{{ t("security_setting.OAuth.Twitter.register_1", "https://apps.twitter.com/", "Twitter Application Management") }}</li>
-    <li>{{ t("security_setting.OAuth.Twitter.register_2") }}</li>
-    <li>{{ t("security_setting.OAuth.Twitter.register_3") }}</li>
-    <li>{{ t("security_setting.OAuth.Twitter.register_4", callbackUrl) }}</li>
-  </ol>
-</div>
-
-<script>
-  $('input[name="settingForm[security:passport-twitter:isEnabled]"]').change(function() {
-      const isEnabled = ($(this).val() === "true");
-
-      if (isEnabled) {
-        $('#passport-twitter-hide-when-disabled').show(400);
-      }
-      else {
-        $('#passport-twitter-hide-when-disabled').hide(400);
-      }
-    });
-</script>
-

+ 1 - 1
src/server/views/widget/alert_siteurl_undefined.html

@@ -1,6 +1,6 @@
 {% if !getConfig('crowi', 'app:siteUrl') %}
 <div class="alert alert-danger text-white mb-0 px-4 py-2">
   <i class="icon-exclamation"></i>
-  {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
+  {{ t("security_setting.alert_siteUrl_is_not_set", { link:t('App Settings')}) }}
 </div>
 {% endif %}

+ 76 - 0
src/test/service/passport.test.js

@@ -0,0 +1,76 @@
+const { getInstance } = require('../setup-crowi');
+
+describe('PassportService test', () => {
+  let crowi;
+
+  beforeEach(async(done) => {
+    crowi = await getInstance();
+    done();
+  });
+
+
+  describe('verifySAMLResponseByABLCRule()', () => {
+
+    let getConfigSpy;
+    let extractAttributesFromSAMLResponseSpy;
+
+    beforeEach(async(done) => {
+      // prepare spy for ConfigManager.getConfig
+      getConfigSpy = jest.spyOn(crowi.configManager, 'getConfig');
+      // prepare spy for extractAttributesFromSAMLResponse method
+      extractAttributesFromSAMLResponseSpy = jest.spyOn(crowi.passportService, 'extractAttributesFromSAMLResponse');
+      done();
+    });
+
+    /* eslint-disable indent */
+    describe.each`
+      conditionId | departments   | positions     | ruleStr                                                         | expected
+      ${1}        | ${[]}         | ${['Leader']} | ${'Position'}                                                   | ${true}
+      ${2}        | ${[]}         | ${['Leader']} | ${'Position: Leader'}                                           | ${true}
+      ${3}        | ${['A']}      | ${[]}         | ${'Department: A || Department: B && Position: Leader'}         | ${true}
+      ${4}        | ${['B']}      | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${true}
+      ${5}        | ${['A', 'C']} | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${true}
+      ${6}        | ${['B', 'C']} | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${true}
+      ${7}        | ${[]}         | ${[]}         | ${'Department: A || Department: B && Position: Leader'}         | ${false}
+      ${8}        | ${['C']}      | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${false}
+      ${9}        | ${['A']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'}       | ${true}
+      ${10}       | ${['B']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'}       | ${true}
+      ${11}       | ${['C']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'}       | ${false}
+      ${12}       | ${['A', 'B']} | ${[]}         | ${'(Department: A || Department: B) && Position: Leader'}       | ${false}
+      ${13}       | ${['A']}      | ${[]}         | ${'Department: A NOT Position: Leader'}                         | ${true}
+      ${14}       | ${['A']}      | ${['Leader']} | ${'Department: A NOT Position: Leader'}                         | ${false}
+      ${15}       | ${[]}         | ${['Leader']} | ${'Department: A OR (Position NOT Position: User)'}             | ${true}
+      ${16}       | ${[]}         | ${['User']}   | ${'Department: A OR (Position NOT Position: User)'}             | ${false}
+    `('to be $expected under rule="$ruleStr"', ({
+      conditionId, departments, positions, ruleStr, expected,
+    }) => {
+      test(`when condition=${conditionId}`, async() => {
+        const responseMock = {};
+
+        // setup mock implementation
+        getConfigSpy.mockImplementation((ns, key) => {
+          if (ns === 'crowi' && key === 'security:passport-saml:ABLCRule') {
+            return ruleStr;
+          }
+          throw new Error('Unexpected behavior.');
+        });
+        extractAttributesFromSAMLResponseSpy.mockImplementation((response) => {
+          if (response !== responseMock) {
+            throw new Error('Unexpected args.');
+          }
+          return {
+            Department: departments,
+            Position: positions,
+          };
+        });
+
+        const result = crowi.passportService.verifySAMLResponseByABLCRule(responseMock);
+
+        expect(result).toBe(expected);
+      });
+    });
+
+  });
+
+
+});

+ 39 - 10
yarn.lock

@@ -1284,6 +1284,11 @@
     "@types/yargs" "^15.0.0"
     chalk "^3.0.0"
 
+"@kaishuu0123/markdown-it-fence@0.1.4", "@kaishuu0123/markdown-it-fence@^0.1.4":
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-0.1.4.tgz#759cf0dd80cca23a08e70b9cbb33c999cb23f3c3"
+  integrity sha512-u00GhVLpTeIbeflMKCozzaCAEmuwGngryomtbsYoyRwdYLfrH2nJHZa41+gwKLXsrq7Ii3N+Rei5GHaqHT3j5A==
+
 "@lykmapipo/common@>=0.21.0":
   version "0.21.0"
   resolved "https://registry.yarnpkg.com/@lykmapipo/common/-/common-0.21.0.tgz#fe06e54e3c02479a8c303fb7843341fee83f60ab"
@@ -8286,6 +8291,11 @@ lru-cache@^5.0.0, lru-cache@^5.1.1:
   dependencies:
     yallist "^3.0.2"
 
+lucene-query-parser@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/lucene-query-parser/-/lucene-query-parser-1.2.0.tgz#46dad5b4ddc59abbf27f9df4c519d959c2033432"
+  integrity sha1-RtrVtN3Fmrvyf530xRnZWcIDNDI=
+
 make-dir@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51"
@@ -8358,24 +8368,29 @@ markdown-escapes@^1.0.0:
   resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.3.tgz#6155e10416efaafab665d466ce598216375195f5"
   integrity sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==
 
-markdown-it-blockdiag@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/markdown-it-blockdiag/-/markdown-it-blockdiag-1.0.2.tgz#30f055a0d5e3dc7195a9055ebbd404381261fe8b"
+markdown-it-blockdiag@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/markdown-it-blockdiag/-/markdown-it-blockdiag-1.0.3.tgz#7c2be967d21a17f559da5860b6179b34f29192a3"
+  integrity sha512-2y4C5L6V3twWhDdQLl3zzYvBiumKcS7D1CD8/OTN2Y57G1gE2ot86vTPrfljaf2Q8q6J1UgcG40zTjDGcHgHrg==
   dependencies:
-    markdown-it-fence "0.1.3"
+    "@kaishuu0123/markdown-it-fence" "0.1.4"
     pako "^1.0.6"
     paths "^0.1.1"
     url-join "^4.0.0"
     utf8-bytes "0.0.1"
 
+markdown-it-drawio-viewer@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.1.3.tgz#e5f7fa9a1d200a8711e2aeb691551add413ffe33"
+  integrity sha512-FELyH+ko9sOAC6hHQqBYi4tI1Qv8HTzVPXsYuwTFy0NF5gY1A9oriN051rHn4UtyAAXEUIMquNNYYkxC9bhMVw==
+  dependencies:
+    "@kaishuu0123/markdown-it-fence" "^0.1.4"
+    xmldoc "^1.1.2"
+
 markdown-it-emoji@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
 
-markdown-it-fence@0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/markdown-it-fence/-/markdown-it-fence-0.1.3.tgz#26e90149b7de5658cdb27096b2e2b5ec4e72d6ce"
-
 markdown-it-footnote@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.1.tgz#7f3730747cacc86e2fe0bf8a17a710f34791517a"
@@ -9925,7 +9940,12 @@ pako@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.3.tgz#5f515b0c6722e1982920ae8005eacb0b7ca73ccf"
 
-pako@^1.0.6, pako@~1.0.5:
+pako@^1.0.6:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+pako@~1.0.5:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
 
@@ -10193,6 +10213,7 @@ path-type@^4.0.0:
 paths@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/paths/-/paths-0.1.1.tgz#9ad909d7f769dd8acb3a1c033c5eef43123d3d17"
+  integrity sha1-mtkJ1/dp3YrLOhwDPF7vQxI9PRc=
 
 pause-stream@0.0.11:
   version "0.0.11"
@@ -11999,7 +12020,7 @@ sax@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
 
-sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
+sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -13906,6 +13927,7 @@ uslug@^1.0.4:
 utf8-bytes@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/utf8-bytes/-/utf8-bytes-0.0.1.tgz#116b025448c9b500081cdfbf1f4d6c6c37d8837d"
+  integrity sha1-EWsCVEjJtQAIHN+/H01sbDfYg30=
 
 util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
@@ -14477,6 +14499,13 @@ xmlchars@^2.1.1:
   resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
   integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
 
+xmldoc@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.2.tgz#6666e029fe25470d599cd30e23ff0d1ed50466d7"
+  integrity sha512-ruPC/fyPNck2BD1dpz0AZZyrEwMOrWTO5lDdIXS91rs3wtm4j+T8Rp2o+zoOYkkAxJTZRPOSnOGei1egoRmKMQ==
+  dependencies:
+    sax "^1.2.1"
+
 xmldom@0.1.27:
   version "0.1.27"
   resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"

Некоторые файлы не были показаны из-за большого количества измененных файлов