Allow configuration to be static/readonly (#4315)

* Allow configuration to be static/readonly

* Make all components disableable

* Improve disabled component styling

* Rename edits allowed field in configuration

* Fix CI
This commit is contained in:
Jelle Besseling 2021-10-12 13:33:44 +02:00 committed by GitHub
parent badacdbb4a
commit 8d8a037e3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 304 additions and 195 deletions

View File

@ -63,7 +63,7 @@
<div class="col-md-7 col-xl-5"></div>
<div class="col-md-5 col-xl-5">
<div class="form-error submit-error" i18n *ngIf="!form.valid">
<div class="form-error submit-error" i18n *ngIf="!form.valid && serverConfig.allowEdits">
There are errors in the form:
<ul>
@ -77,7 +77,11 @@
You cannot allow live replay if you don't enable transcoding.
</span>
<input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid || !hasConsistentOptions()">
<span i18n *ngIf="!serverConfig.allowEdits">
You cannot change the server configuration because it's managed externally.
</span>
<input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid || !hasConsistentOptions() || !serverConfig.allowEdits">
</div>
</div>
</form>

View File

@ -33,6 +33,11 @@ input[type=number] {
top: 5px;
right: 2.5rem;
}
input[disabled] {
background-color: #f9f9f9;
pointer-events: none;
}
}
input[type=checkbox] {
@ -93,6 +98,11 @@ textarea {
}
}
input[disabled] {
opacity: 0.5;
}
.form-group-right {
padding-top: 2px;
}

View File

@ -258,6 +258,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
this.loadConfigAndUpdateForm()
this.loadCategoriesAndLanguages()
if (!this.serverConfig.allowEdits) {
this.form.disable()
}
}
formValidated () {

View File

@ -2,6 +2,7 @@
<textarea #textarea
[(ngModel)]="content" (ngModelChange)="onModelChange()"
class="form-control" [ngClass]="classes"
[attr.disabled]="disabled"
[ngStyle]="{ height: textareaHeight }"
[id]="name" [name]="name">
</textarea>
@ -25,11 +26,11 @@
</ng-container>
<my-button
*ngIf="!isMaximized" [title]="maximizeInText" className="maximize-button" icon="fullscreen" (click)="onMaximizeClick()"
*ngIf="!isMaximized" [title]="maximizeInText" className="maximize-button" icon="fullscreen" (click)="onMaximizeClick()" [disabled]="disabled"
></my-button>
<my-button
*ngIf="isMaximized" [title]="maximizeOutText" className="maximize-button" icon="exit-fullscreen" (click)="onMaximizeClick()"
*ngIf="isMaximized" [title]="maximizeOutText" className="maximize-button" icon="exit-fullscreen" (click)="onMaximizeClick()" [disabled]="disabled"
></my-button>
</div>

View File

@ -45,6 +45,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
previewHTML: SafeHtml | string = ''
isMaximized = false
disabled = false
maximizeInText = $localize`Maximize editor`
maximizeOutText = $localize`Exit maximized editor`
@ -108,6 +109,10 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
}
}
setDisabledState (isDisabled: boolean) {
this.disabled = isDisabled
}
private lockBodyScroll () {
this.scrollPosition = this.viewportScroller.getScrollPosition()
document.getElementById('content').classList.add('lock-scroll')

View File

@ -7,6 +7,7 @@
[multiple]="true"
[searchable]="true"
[closeOnSelect]="false"
[disabled]="disabled"
bindValue="id"
bindLabel="label"

View File

@ -23,6 +23,8 @@ export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
@Input() selectableGroupAsModel: boolean
@Input() placeholder: string
disabled = false
ngOnInit () {
if (!this.placeholder) this.placeholder = $localize`Add a new option`
}
@ -59,6 +61,10 @@ export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
this.propagateChange(this.selectedItems)
}
setDisabledState (isDisabled: boolean) {
this.disabled = isDisabled
}
compareFn (item: SelectOptionsItem, selected: ItemSelectCheckboxValue) {
if (typeof selected === 'string' || typeof selected === 'number') {
return item.id === selected

View File

@ -5,6 +5,7 @@
[searchable]="searchable"
[groupBy]="groupBy"
[labelForId]="labelForId"
[disabled]="disabled"
[(ngModel)]="selectedId"
(ngModelChange)="onModelChange()"

View File

@ -25,6 +25,7 @@ export class SelectCustomValueComponent implements ControlValueAccessor, OnChang
customValue: number | string = ''
selectedId: number | string
disabled = false
itemsWithCustom: SelectOptionsItem[] = []
@ -75,4 +76,8 @@ export class SelectCustomValueComponent implements ControlValueAccessor, OnChang
isCustomValue () {
return this.selectedId === 'other'
}
setDisabledState (isDisabled: boolean) {
this.disabled = isDisabled
}
}

View File

@ -7,6 +7,7 @@
[labelForId]="labelForId"
[searchable]="searchable"
[searchFn]="searchFn"
[disabled]="disabled"
bindLabel="label"
bindValue="id"

View File

@ -23,6 +23,7 @@ export class SelectOptionsComponent implements ControlValueAccessor {
@Input() searchFn: any
selectedId: number | string
disabled = false
propagateChange = (_: any) => { /* empty */ }
@ -48,4 +49,8 @@ export class SelectOptionsComponent implements ControlValueAccessor {
onModelChange () {
this.propagateChange(this.selectedId)
}
setDisabledState (isDisabled: boolean) {
this.disabled = isDisabled
}
}

View File

@ -364,6 +364,9 @@
cursor: default;
}
}
select[disabled] {
background-color: #f9f9f9;
}
@media screen and (max-width: $width) {
width: 100%;

View File

@ -243,6 +243,11 @@ peertube:
# You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
url: 'https://joinpeertube.org/api/v1/versions.json'
webadmin:
configuration:
edit:
allowed: true
cache:
previews:
size: 500 # Max number of previews you want to cache

View File

@ -241,6 +241,11 @@ peertube:
# You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
url: 'https://joinpeertube.org/api/v1/versions.json'
webadmin:
configuration:
# Set to false if you want the config to be readonly
allow_edits: true
###############################################################################
#
# From this point, all the following keys can be overridden by the web interface

View File

@ -11,7 +11,7 @@ import { objectConverter } from '../../helpers/core-utils'
import { CONFIG, reloadConfig } from '../../initializers/config'
import { ClientHtml } from '../../lib/client-html'
import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares'
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config'
const configRouter = express.Router()
@ -38,6 +38,7 @@ configRouter.put('/custom',
openapiOperationDoc({ operationId: 'putCustomConfig' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
ensureConfigIsEditable,
customConfigUpdateValidator,
asyncMiddleware(updateCustomConfig)
)
@ -46,6 +47,7 @@ configRouter.delete('/custom',
openapiOperationDoc({ operationId: 'delCustomConfig' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
ensureConfigIsEditable,
asyncMiddleware(deleteCustomConfig)
)

View File

@ -195,6 +195,13 @@ const CONFIG = {
URL: config.get<string>('peertube.check_latest_version.url')
}
},
WEBADMIN: {
CONFIGURATION: {
EDITS: {
ALLOWED: config.get<boolean>('webadmin.configuration.edit.allowed')
}
}
},
ADMIN: {
get EMAIL () { return config.get<string>('admin.email') }
},
@ -411,14 +418,22 @@ export {
// ---------------------------------------------------------------------------
function getLocalConfigFilePath () {
const configSources = config.util.getConfigSources()
if (configSources.length === 0) throw new Error('Invalid config source.')
const localConfigDir = getLocalConfigDir()
let filename = 'local'
if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
return join(dirname(configSources[0].name), filename + '.json')
return join(localConfigDir, filename + '.json')
}
function getLocalConfigDir () {
if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG
const configSources = config.util.getConfigSources()
if (configSources.length === 0) throw new Error('Invalid config source.')
return dirname(configSources[0].name)
}
function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
@ -437,19 +452,19 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
export function reloadConfig () {
function getConfigDirectory () {
function getConfigDirectories () {
if (process.env.NODE_CONFIG_DIR) {
return process.env.NODE_CONFIG_DIR
return process.env.NODE_CONFIG_DIR.split(":")
}
return join(root(), 'config')
return [ join(root(), 'config') ]
}
function purge () {
const directory = getConfigDirectory()
const directories = getConfigDirectories()
for (const fileName in require.cache) {
if (fileName.includes(directory) === false) {
if (directories.some((dir) => fileName.includes(dir)) === false) {
continue
}

View File

@ -42,6 +42,7 @@ class ServerConfigManager {
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
return {
allowEdits: CONFIG.WEBADMIN.CONFIGURATION.EDITS.ALLOWED,
instance: {
name: CONFIG.INSTANCE.NAME,
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,

View File

@ -1,13 +1,14 @@
import express from 'express'
import { body } from 'express-validator'
import { isIntOrNull } from '@server/helpers/custom-validators/misc'
import { isEmailEnabled } from '@server/initializers/config'
import { CONFIG, isEmailEnabled } from '@server/initializers/config'
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
import { logger } from '../../helpers/logger'
import { isThemeRegistered } from '../../lib/plugins/theme-utils'
import { areValidationErrors } from './shared'
import { HttpStatusCode } from '@shared/models/http/http-error-codes'
const customConfigUpdateValidator = [
body('instance.name').exists().withMessage('Should have a valid instance name'),
@ -104,10 +105,21 @@ const customConfigUpdateValidator = [
}
]
function ensureConfigIsEditable (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!CONFIG.WEBADMIN.CONFIGURATION.EDITS.ALLOWED) {
return res.fail({
status: HttpStatusCode.METHOD_NOT_ALLOWED_405,
message: 'Server configuration is static and cannot be edited'
})
}
return next()
}
// ---------------------------------------------------------------------------
export {
customConfigUpdateValidator
customConfigUpdateValidator,
ensureConfigIsEditable
}
function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {

View File

@ -201,57 +201,6 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.broadcastMessage.dismissable).to.be.true
}
describe('Test config', function () {
let server: PeerTubeServer = null
before(async function () {
this.timeout(30000)
server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
})
it('Should have a correct config on a server with registration enabled', async function () {
const data = await server.config.getConfig()
expect(data.signup.allowed).to.be.true
})
it('Should have a correct config on a server with registration enabled and a users limit', async function () {
this.timeout(5000)
await Promise.all([
server.users.register({ username: 'user1' }),
server.users.register({ username: 'user2' }),
server.users.register({ username: 'user3' })
])
const data = await server.config.getConfig()
expect(data.signup.allowed).to.be.false
})
it('Should have the correct video allowed extensions', async function () {
const data = await server.config.getConfig()
expect(data.video.file.extensions).to.have.lengthOf(3)
expect(data.video.file.extensions).to.contain('.mp4')
expect(data.video.file.extensions).to.contain('.webm')
expect(data.video.file.extensions).to.contain('.ogv')
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
expect(data.contactForm.enabled).to.be.true
})
it('Should get the customized configuration', async function () {
const data = await server.config.getCustomConfig()
checkInitialConfig(server, data)
})
it('Should update the customized configuration', async function () {
const newCustomConfig: CustomConfig = {
instance: {
name: 'PeerTube updated',
@ -423,6 +372,79 @@ describe('Test config', function () {
}
}
}
describe('Test static config', function () {
let server: PeerTubeServer = null
before(async function () {
this.timeout(30000)
server = await createSingleServer(1, { webadmin: { configuration: { edit: { allowed: false } } } })
await setAccessTokensToServers([ server ])
})
it('Should tell the client that edits are not allowed', async function () {
const data = await server.config.getConfig()
expect(data.allowEdits).to.be.false
})
it('Should error when client tries to update', async function () {
await server.config.updateCustomConfig({ newCustomConfig, expectedStatus: 405 })
})
})
describe('Test config', function () {
let server: PeerTubeServer = null
before(async function () {
this.timeout(30000)
server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
})
it('Should have a correct config on a server with registration enabled', async function () {
const data = await server.config.getConfig()
expect(data.signup.allowed).to.be.true
})
it('Should have a correct config on a server with registration enabled and a users limit', async function () {
this.timeout(5000)
await Promise.all([
server.users.register({ username: 'user1' }),
server.users.register({ username: 'user2' }),
server.users.register({ username: 'user3' })
])
const data = await server.config.getConfig()
expect(data.signup.allowed).to.be.false
})
it('Should have the correct video allowed extensions', async function () {
const data = await server.config.getConfig()
expect(data.video.file.extensions).to.have.lengthOf(3)
expect(data.video.file.extensions).to.contain('.mp4')
expect(data.video.file.extensions).to.contain('.webm')
expect(data.video.file.extensions).to.contain('.ogv')
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
expect(data.contactForm.enabled).to.be.true
})
it('Should get the customized configuration', async function () {
const data = await server.config.getCustomConfig()
checkInitialConfig(server, data)
})
it('Should update the customized configuration', async function () {
await server.config.updateCustomConfig({ newCustomConfig })
const data = await server.config.getCustomConfig()

View File

@ -30,6 +30,7 @@ export interface RegisteredIdAndPassAuthConfig {
}
export interface ServerConfig {
allowEdits: boolean
serverVersion: string
serverCommit?: string

View File

@ -33,7 +33,8 @@ RUN mkdir /data /config
RUN chown -R peertube:peertube /data /config
ENV NODE_ENV production
ENV NODE_CONFIG_DIR /config
ENV NODE_CONFIG_DIR /app/config:/app/support/docker/production/config:/config
ENV PEERTUBE_LOCAL_CONFIG /config
VOLUME /data
VOLUME /config

View File

@ -68,6 +68,13 @@ object_storage:
prefix: "PEERTUBE_OBJECT_STORAGE_VIDEOS_PREFIX"
base_url: "PEERTUBE_OBJECT_STORAGE_VIDEOS_BASE_URL"
webadmin:
configuration:
edit:
allowed:
__name: "PEERTUBE_ALLOW_WEBADMIN_CONFIG"
__format: "json"
log:
level: "PEERTUBE_LOG_LEVEL"
log_ping_requests:

View File

@ -1,15 +1,8 @@
#!/bin/sh
set -e
# Populate config directory
if [ -z "$(ls -A /config)" ]; then
cp /app/support/docker/production/config/* /config
fi
# Always copy default and custom env configuration file, in cases where new keys were added
cp /app/config/default.yaml /config
cp /app/support/docker/production/config/custom-environment-variables.yaml /config
find /config ! -user peertube -exec chown peertube:peertube {} \;
find /config ! -user peertube -exec chown peertube:peertube {} \; || true
# first arg is `-f` or `--some-option`
# or first arg is `something.conf`