admin-security.vue 17.4 KB
Newer Older
1 2 3 4 5
<template lang='pug'>
  v-container(fluid, grid-list-lg)
    v-layout(row wrap)
      v-flex(xs12)
        .admin-header
6
          img.animated.fadeInUp(src='/_assets/svg/icon-private.svg', alt='Security', style='width: 80px;')
7 8 9 10 11 12 13 14 15 16 17 18 19
          .admin-header-title
            .headline.primary--text.animated.fadeInLeft {{ $t('admin:security.title') }}
            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:security.subtitle') }}
          v-spacer
          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
            v-icon(left) mdi-check
            span {{$t('common:actions.apply')}}
        v-form.pt-3
          v-layout(row wrap)
            v-flex(lg6 xs12)
              v-card.animated.fadeInUp
                v-toolbar(color='red darken-2', dark, dense, flat)
                  v-toolbar-title.subtitle-1 Security
NGPixel's avatar
NGPixel committed
20 21
                v-card-info(color='red')
                  span Make sure to understand the implications before turning on / off a security feature.
22
                v-card-text
NGPixel's avatar
NGPixel committed
23
                  v-switch(
24 25 26 27 28 29 30 31
                    inset
                    label='Block Open Redirect'
                    color='red darken-2'
                    v-model='config.securityOpenRedirect'
                    persistent-hint
                    hint='Prevents user controlled URLs from directing to websites outside of your wiki. This provides Open Redirect protection.'
                    )

NGPixel's avatar
NGPixel committed
32
                  v-divider.mt-3
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
                  v-switch.mt-3(
                    inset
                    label='Block IFrame Embedding'
                    color='red darken-2'
                    v-model='config.securityIframe'
                    persistent-hint
                    hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'
                    )

                  v-divider.mt-3
                  v-switch(
                    inset
                    label='Same Origin Referrer Policy'
                    color='red darken-2'
                    v-model='config.securityReferrerPolicy'
                    persistent-hint
                    hint='Limits the referrer header to same origin.'
                    )

                  v-divider.mt-3
                  v-switch(
                    inset
                    label='Trust X-Forwarded-* Proxy Headers'
                    color='red darken-2'
                    v-model='config.securityTrustProxy'
                    persistent-hint
                    hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.'
                    )

                  //- v-divider.mt-3
                  //- v-switch(
                  //-   inset
                  //-   label='Subresource Integrity (SRI)'
                  //-   color='red darken-2'
                  //-   v-model='config.securitySRI'
                  //-   persistent-hint
                  //-   hint='This ensure that resources such as CSS and JS files are not altered during delivery.'
                  //-   disabled
                  //-   )

                  v-divider.mt-3
                  v-switch(
                    inset
                    label='Enforce HSTS'
                    color='red darken-2'
                    v-model='config.securityHSTS'
                    persistent-hint
                    hint='This ensures the connection cannot be established through an insecure HTTP connection.'
                    )
                  v-select.mt-5(
                    outlined
                    label='HSTS Max Age'
                    :items='hstsDurations'
                    v-model='config.securityHSTSDuration'
                    prepend-icon='mdi-subdirectory-arrow-right'
                    :disabled='!config.securityHSTS'
                    hide-details
                    style='max-width: 450px;'
                    )
                  .pl-11.mt-3
                    .caption Defines the duration for which the server should only deliver content through HTTPS.
                    .caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values.

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
                  //- v-divider.mt-3
                  //- v-switch(
                  //-   inset
                  //-   label='Enforce CSP'
                  //-   color='red darken-2'
                  //-   v-model='config.securityCSP'
                  //-   persistent-hint
                  //-   hint='Restricts scripts to pre-approved content sources.'
                  //-   disabled
                  //-   )
                  //- v-textarea.mt-5(
                  //-   label='CSP Directives'
                  //-   outlined
                  //-   v-model='config.securityCSPDirectives'
                  //-   prepend-icon='mdi-subdirectory-arrow-right'
                  //-   persistent-hint
                  //-   hint='One directive per line.'
                  //-   disabled
                  //- )
115 116 117 118 119

            v-flex(lg6 xs12)
              v-card.animated.fadeInUp.wait-p2s
                v-toolbar(color='primary', dark, dense, flat)
                  v-toolbar-title.subtitle-1 {{ $t('admin:security.uploads') }}
NGPixel's avatar
NGPixel committed
120 121
                v-card-info(color='blue')
                  span {{$t('admin:security.uploadsInfo')}}
122
                v-card-text
NGPixel's avatar
NGPixel committed
123
                  v-text-field.mt-3(
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
                    outlined
                    :label='$t(`admin:security.maxUploadSize`)'
                    required
                    v-model='config.uploadMaxFileSize'
                    prepend-icon='mdi-progress-upload'
                    :hint='$t(`admin:security.maxUploadSizeHint`)'
                    persistent-hint
                    :suffix='$t(`admin:security.maxUploadSizeSuffix`)'
                    style='max-width: 450px;'
                    )
                  v-text-field.mt-3(
                    outlined
                    :label='$t(`admin:security.maxUploadBatch`)'
                    required
                    v-model='config.uploadMaxFiles'
                    prepend-icon='mdi-upload-lock'
                    :hint='$t(`admin:security.maxUploadBatchHint`)'
                    persistent-hint
                    :suffix='$t(`admin:security.maxUploadBatchSuffix`)'
                    style='max-width: 450px;'
                    )
NGPixel's avatar
NGPixel committed
145 146 147 148 149 150 151 152 153
                  v-divider.mt-3
                  v-switch(
                    inset
                    label='Scan and Sanitize SVG Uploads'
                    color='primary'
                    v-model='config.uploadScanSVG'
                    persistent-hint
                    hint='Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.'
                    )
154 155 156 157 158 159 160 161 162
                  v-divider.mt-3
                  v-switch(
                    inset
                    label='Force Download of Unsafe Extensions'
                    color='primary'
                    v-model='config.uploadForceDownload'
                    persistent-hint
                    hint='Should non-image files be forced as downloads when accessed directly. This prevents potential XSS attacks via unsafe file extensions uploads.'
                    )
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183

              v-card.mt-3.animated.fadeInUp.wait-p2s
                v-toolbar(flat, color='primary', dark, dense)
                  .subtitle-1 {{$t('admin:security.login')}}
                //- v-card-info(color='blue')
                //-   span {{$t('admin:security.loginInfo')}}
                .overline.grey--text.pa-4 {{$t('admin:security.loginScreen')}}
                .px-4.pb-3
                  v-text-field(
                    outlined
                    :label='$t(`admin:security.loginBgUrl`)'
                    v-model='config.authLoginBgUrl'
                    :hint='$t(`admin:security.loginBgUrlHint`)'
                    persistent-hint
                    prepend-icon='mdi-image-area'
                    append-icon='mdi-folder-image'
                    @click:append='browseLoginBg'
                  )
                  v-switch(
                    inset
                    :label='$t(`admin:security.bypassLogin`)'
184
                    color='primary'
185 186 187 188 189
                    v-model='config.authAutoLogin'
                    prepend-icon='mdi-fast-forward'
                    persistent-hint
                    :hint='$t(`admin:security.bypassLoginHint`)'
                    )
190 191 192 193 194 195 196 197 198
                  v-switch(
                    inset
                    :label='$t(`admin:security.hideLocalLogin`)'
                    color='primary'
                    v-model='config.authHideLocal'
                    prepend-icon='mdi-eye-off-outline'
                    persistent-hint
                    :hint='$t(`admin:security.hideLocalLoginHint`)'
                    )
199
                v-divider.mt-3
200 201 202 203 204 205 206 207 208 209 210 211
                .overline.grey--text.pa-4 {{$t('admin:security.loginSecurity')}}
                .px-4.pb-3
                  v-switch.mt-0(
                    inset
                    :label='$t(`admin:security.enforce2fa`)'
                    color='primary'
                    v-model='config.authEnforce2FA'
                    prepend-icon='mdi-two-factor-authentication'
                    :hint='$t(`admin:security.enforce2faHint`)'
                    persistent-hint
                  )
                v-divider.mt-3
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
                .overline.grey--text.pa-4 {{$t('admin:security.jwt')}}
                .px-4.pb-3
                  v-text-field(
                    v-model='config.authJwtAudience'
                    outlined
                    prepend-icon='mdi-account-group-outline'
                    :label='$t(`admin:auth.jwtAudience`)'
                    :hint='$t(`admin:auth.jwtAudienceHint`)'
                    persistent-hint
                  )
                  v-text-field.mt-3(
                    v-model='config.authJwtExpiration'
                    outlined
                    prepend-icon='mdi-clock-outline'
                    :label='$t(`admin:auth.tokenExpiration`)'
                    :hint='$t(`admin:auth.tokenExpirationHint`)'
                    persistent-hint
                  )
                  v-text-field.mt-3(
                    v-model='config.authJwtRenewablePeriod'
                    outlined
                    prepend-icon='mdi-update'
                    :label='$t(`admin:auth.tokenRenewalPeriod`)'
                    :hint='$t(`admin:auth.tokenRenewalPeriodHint`)'
                    persistent-hint
                  )

    component(:is='activeModal')
240 241 242 243 244 245 246
</template>

<script>
import _ from 'lodash'
import { sync } from 'vuex-pathify'
import gql from 'graphql-tag'

247 248 249 250 251 252
import editorStore from '../../store/editor'

/* global WIKI */

WIKI.$store.registerModule('editor', editorStore)

253
export default {
254 255 256 257
  i18nOptions: { namespaces: 'editor' },
  components: {
    editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
  },
258 259 260 261 262
  data() {
    return {
      config: {
        uploadMaxFileSize: 0,
        uploadMaxFiles: 0,
NGPixel's avatar
NGPixel committed
263
        uploadScanSVG: true,
264
        uploadForceDownload: true,
265
        securityOpenRedirect: true,
266 267 268 269 270 271 272
        securityIframe: true,
        securityReferrerPolicy: true,
        securityTrustProxy: true,
        securitySRI: true,
        securityHSTS: false,
        securityHSTSDuration: 0,
        securityCSP: false,
273 274
        securityCSPDirectives: '',
        authAutoLogin: false,
275
        authHideLocal: false,
276 277 278 279
        authLoginBgUrl: '',
        authJwtAudience: 'urn:wiki.js',
        authJwtExpiration: '30m',
        authJwtRenewablePeriod: '14d'
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
      },
      hstsDurations: [
        { value: 300, text: '5 minutes' },
        { value: 86400, text: '1 day' },
        { value: 604800, text: '1 week' },
        { value: 2592000, text: '1 month' },
        { value: 31536000, text: '1 year' },
        { value: 63072000, text: '2 years' }
      ]
    }
  },
  computed: {
    activeModal: sync('editor/activeModal')
  },
  methods: {
    async save () {
      try {
        await this.$apollo.mutate({
          mutation: gql`
            mutation (
300
              $authAutoLogin: Boolean
301
              $authEnforce2FA: Boolean
302
              $authHideLocal: Boolean
303 304 305 306
              $authLoginBgUrl: String
              $authJwtAudience: String
              $authJwtExpiration: String
              $authJwtRenewablePeriod: String
307 308
              $uploadMaxFileSize: Int
              $uploadMaxFiles: Int
NGPixel's avatar
NGPixel committed
309
              $uploadScanSVG: Boolean
310
              $uploadForceDownload: Boolean
311
              $securityOpenRedirect: Boolean
312 313 314 315 316 317 318 319 320 321 322
              $securityIframe: Boolean
              $securityReferrerPolicy: Boolean
              $securityTrustProxy: Boolean
              $securitySRI: Boolean
              $securityHSTS: Boolean
              $securityHSTSDuration: Int
              $securityCSP: Boolean
              $securityCSPDirectives: String
            ) {
              site {
                updateConfig(
323
                  authAutoLogin: $authAutoLogin,
324
                  authEnforce2FA: $authEnforce2FA,
325
                  authHideLocal: $authHideLocal,
326 327 328 329
                  authLoginBgUrl: $authLoginBgUrl,
                  authJwtAudience: $authJwtAudience,
                  authJwtExpiration: $authJwtExpiration,
                  authJwtRenewablePeriod: $authJwtRenewablePeriod,
330 331
                  uploadMaxFileSize: $uploadMaxFileSize,
                  uploadMaxFiles: $uploadMaxFiles,
NGPixel's avatar
NGPixel committed
332
                  uploadScanSVG: $uploadScanSVG
333
                  uploadForceDownload: $uploadForceDownload,
334
                  securityOpenRedirect: $securityOpenRedirect,
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
                  securityIframe: $securityIframe,
                  securityReferrerPolicy: $securityReferrerPolicy,
                  securityTrustProxy: $securityTrustProxy,
                  securitySRI: $securitySRI,
                  securityHSTS: $securityHSTS,
                  securityHSTSDuration: $securityHSTSDuration,
                  securityCSP: $securityCSP,
                  securityCSPDirectives: $securityCSPDirectives
                ) {
                  responseResult {
                    succeeded
                    errorCode
                    slug
                    message
                  }
                }
              }
            }
          `,
          variables: {
355
            authAutoLogin: _.get(this.config, 'authAutoLogin', false),
356
            authEnforce2FA: _.get(this.config, 'authEnforce2FA', false),
357
            authHideLocal: _.get(this.config, 'authHideLocal', false),
358 359 360 361
            authLoginBgUrl: _.get(this.config, 'authLoginBgUrl', ''),
            authJwtAudience: _.get(this.config, 'authJwtAudience', ''),
            authJwtExpiration: _.get(this.config, 'authJwtExpiration', ''),
            authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),
362 363
            uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),
            uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),
NGPixel's avatar
NGPixel committed
364
            uploadScanSVG: _.get(this.config, 'uploadScanSVG', false),
365
            uploadForceDownload: _.get(this.config, 'uploadForceDownload', false),
366
            securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
            securityIframe: _.get(this.config, 'securityIframe', false),
            securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
            securityTrustProxy: _.get(this.config, 'securityTrustProxy', false),
            securitySRI: _.get(this.config, 'securitySRI', false),
            securityHSTS: _.get(this.config, 'securityHSTS', false),
            securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),
            securityCSP: _.get(this.config, 'securityCSP', false),
            securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: 'Configuration saved successfully.',
          icon: 'check'
        })
      } catch (err) {
        this.$store.commit('pushGraphError', err)
      }
388 389 390 391
    },
    browseLoginBg () {
      this.$store.set('editor/editorKey', 'common')
      this.activeModal = 'editorModalMedia'
392 393
    }
  },
394 395
  mounted () {
    this.$root.$on('editorInsert', opts => {
396
      this.config.authLoginBgUrl = opts.path
397 398 399 400 401
    })
  },
  beforeDestroy() {
    this.$root.$off('editorInsert')
  },
402 403 404 405 406 407
  apollo: {
    config: {
      query: gql`
        {
          site {
            config {
408
              authAutoLogin
409
              authEnforce2FA
410
              authHideLocal
411 412 413 414
              authLoginBgUrl
              authJwtAudience
              authJwtExpiration
              authJwtRenewablePeriod
415 416
              uploadMaxFileSize
              uploadMaxFiles
NGPixel's avatar
NGPixel committed
417
              uploadScanSVG
418
              uploadForceDownload
419
              securityOpenRedirect
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
              securityIframe
              securityReferrerPolicy
              securityTrustProxy
              securitySRI
              securityHSTS
              securityHSTSDuration
              securityCSP
              securityCSPDirectives
            }
          }
        }
      `,
      fetchPolicy: 'network-only',
      update: (data) => _.cloneDeep(data.site.config),
      watchLoading (isLoading) {
        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-security-refresh')
      }
    }
  }
}
</script>

<style lang='scss'>

</style>