Merge pull request #1645 from symonbaikov/feature/apprise-config-options
feat: add operator config options for Apprise notifications (closes #1576)
This commit is contained in:
@@ -42,6 +42,15 @@ SECRET_KEY=notsecretkey
|
|||||||
# will be sent through this proxy if set.
|
# will be sent through this proxy if set.
|
||||||
# OUTGOING_PROXY=http://proxy:3128
|
# OUTGOING_PROXY=http://proxy:3128
|
||||||
|
|
||||||
|
# Apprise notifications configuration.
|
||||||
|
# Set to false to prevent users from configuring and sending Apprise notifications.
|
||||||
|
# APPRISE_ENABLED=false
|
||||||
|
# Comma-separated allowlist of Apprise schemas users may use (e.g. slack,telegram).
|
||||||
|
# When set, only schemas in this list are accepted; APPRISE_BLOCKED_SCHEMAS is ignored.
|
||||||
|
# APPRISE_ALLOWED_SCHEMAS=
|
||||||
|
# Comma-separated blocklist of Apprise schemas. Replaces the built-in blocked list.
|
||||||
|
# APPRISE_BLOCKED_SCHEMAS=syslog,dbus,kde,qt,glib,gnome,macosx,windows
|
||||||
|
|
||||||
# Set to true to expose the Swagger specification at /swagger.json
|
# Set to true to expose the Swagger specification at /swagger.json
|
||||||
# SWAGGER_EXPOSED=false
|
# SWAGGER_EXPOSED=false
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ const Errors = {
|
|||||||
BOARD_NOT_FOUND: {
|
BOARD_NOT_FOUND: {
|
||||||
boardNotFound: 'Board not found',
|
boardNotFound: 'Board not found',
|
||||||
},
|
},
|
||||||
|
APPRISE_DISABLED: {
|
||||||
|
appriseDisabled: 'Apprise notifications are disabled',
|
||||||
|
},
|
||||||
|
SCHEMA_NOT_ALLOWED: {
|
||||||
|
schemaNotAllowed: 'Notification service schema is not allowed',
|
||||||
|
},
|
||||||
LIMIT_REACHED: {
|
LIMIT_REACHED: {
|
||||||
limitReached: 'Limit reached',
|
limitReached: 'Limit reached',
|
||||||
},
|
},
|
||||||
@@ -95,6 +101,12 @@ module.exports = {
|
|||||||
boardNotFound: {
|
boardNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
appriseDisabled: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
|
schemaNotAllowed: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
limitReached: {
|
limitReached: {
|
||||||
responseType: 'conflict',
|
responseType: 'conflict',
|
||||||
},
|
},
|
||||||
@@ -125,6 +137,8 @@ module.exports = {
|
|||||||
actorUser: currentUser,
|
actorUser: currentUser,
|
||||||
request: this.req,
|
request: this.req,
|
||||||
})
|
})
|
||||||
|
.intercept('appriseDisabled', () => Errors.APPRISE_DISABLED)
|
||||||
|
.intercept('schemaNotAllowed', () => Errors.SCHEMA_NOT_ALLOWED)
|
||||||
.intercept('limitReached', () => Errors.LIMIT_REACHED);
|
.intercept('limitReached', () => Errors.LIMIT_REACHED);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ const Errors = {
|
|||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
},
|
},
|
||||||
|
APPRISE_DISABLED: {
|
||||||
|
appriseDisabled: 'Apprise notifications are disabled',
|
||||||
|
},
|
||||||
|
SCHEMA_NOT_ALLOWED: {
|
||||||
|
schemaNotAllowed: 'Notification service schema is not allowed',
|
||||||
|
},
|
||||||
LIMIT_REACHED: {
|
LIMIT_REACHED: {
|
||||||
limitReached: 'Limit reached',
|
limitReached: 'Limit reached',
|
||||||
},
|
},
|
||||||
@@ -95,6 +101,12 @@ module.exports = {
|
|||||||
userNotFound: {
|
userNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
|
appriseDisabled: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
|
schemaNotAllowed: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
limitReached: {
|
limitReached: {
|
||||||
responseType: 'conflict',
|
responseType: 'conflict',
|
||||||
},
|
},
|
||||||
@@ -118,6 +130,8 @@ module.exports = {
|
|||||||
actorUser: currentUser,
|
actorUser: currentUser,
|
||||||
request: this.req,
|
request: this.req,
|
||||||
})
|
})
|
||||||
|
.intercept('appriseDisabled', () => Errors.APPRISE_DISABLED)
|
||||||
|
.intercept('schemaNotAllowed', () => Errors.SCHEMA_NOT_ALLOWED)
|
||||||
.intercept('limitReached', () => Errors.LIMIT_REACHED);
|
.intercept('limitReached', () => Errors.LIMIT_REACHED);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -23,11 +23,28 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
appriseDisabled: {},
|
||||||
|
schemaNotAllowed: {},
|
||||||
limitReached: {},
|
limitReached: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const { values } = inputs;
|
const { values } = inputs;
|
||||||
|
const { appriseEnabled, appriseAllowedSchemas, appriseBlockedSchemas } = sails.config.custom;
|
||||||
|
|
||||||
|
if (!appriseEnabled) {
|
||||||
|
throw 'appriseDisabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = values.url.split(':')[0];
|
||||||
|
|
||||||
|
if (appriseAllowedSchemas.length > 0 && !appriseAllowedSchemas.includes(schema)) {
|
||||||
|
throw 'schemaNotAllowed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appriseAllowedSchemas.length === 0 && appriseBlockedSchemas.includes(schema)) {
|
||||||
|
throw 'schemaNotAllowed';
|
||||||
|
}
|
||||||
|
|
||||||
const notificationServicesTotal = await sails.helpers.boards.getNotificationServicesTotal(
|
const notificationServicesTotal = await sails.helpers.boards.getNotificationServicesTotal(
|
||||||
values.board.id,
|
values.board.id,
|
||||||
|
|||||||
@@ -19,11 +19,28 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
appriseDisabled: {},
|
||||||
|
schemaNotAllowed: {},
|
||||||
limitReached: {},
|
limitReached: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const { values } = inputs;
|
const { values } = inputs;
|
||||||
|
const { appriseEnabled, appriseAllowedSchemas, appriseBlockedSchemas } = sails.config.custom;
|
||||||
|
|
||||||
|
if (!appriseEnabled) {
|
||||||
|
throw 'appriseDisabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = values.url.split(':')[0];
|
||||||
|
|
||||||
|
if (appriseAllowedSchemas.length > 0 && !appriseAllowedSchemas.includes(schema)) {
|
||||||
|
throw 'schemaNotAllowed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appriseAllowedSchemas.length === 0 && appriseBlockedSchemas.includes(schema)) {
|
||||||
|
throw 'schemaNotAllowed';
|
||||||
|
}
|
||||||
|
|
||||||
const notificationServicesTotal = await sails.helpers.users.getNotificationServicesTotal(
|
const notificationServicesTotal = await sails.helpers.users.getNotificationServicesTotal(
|
||||||
values.user.id,
|
values.user.id,
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
|
const { appriseEnabled, appriseAllowedSchemas, appriseBlockedSchemas } = sails.config.custom;
|
||||||
|
|
||||||
|
if (!appriseEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaConfig = JSON.stringify({
|
||||||
|
allowedSchemas: appriseAllowedSchemas,
|
||||||
|
blockedSchemas: appriseBlockedSchemas,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await promisifyExecFile(
|
await promisifyExecFile(
|
||||||
PYTHON_PATH,
|
PYTHON_PATH,
|
||||||
@@ -39,6 +50,7 @@ module.exports = {
|
|||||||
JSON.stringify(inputs.services),
|
JSON.stringify(inputs.services),
|
||||||
inputs.title,
|
inputs.title,
|
||||||
JSON.stringify(inputs.bodyByFormat),
|
JSON.stringify(inputs.bodyByFormat),
|
||||||
|
schemaConfig,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ module.exports.custom = {
|
|||||||
smtpPassword: process.env.SMTP_PASSWORD,
|
smtpPassword: process.env.SMTP_PASSWORD,
|
||||||
smtpFrom: process.env.SMTP_FROM,
|
smtpFrom: process.env.SMTP_FROM,
|
||||||
|
|
||||||
|
appriseEnabled: process.env.APPRISE_ENABLED !== 'false',
|
||||||
|
appriseAllowedSchemas: envToArray(process.env.APPRISE_ALLOWED_SCHEMAS),
|
||||||
|
appriseBlockedSchemas: envToArray(process.env.APPRISE_BLOCKED_SCHEMAS),
|
||||||
|
|
||||||
gravatarBaseUrl: process.env.GRAVATAR_BASE_URL,
|
gravatarBaseUrl: process.env.GRAVATAR_BASE_URL,
|
||||||
|
|
||||||
/* Internal */
|
/* Internal */
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import logging
|
|||||||
import apprise
|
import apprise
|
||||||
|
|
||||||
|
|
||||||
BLOCKED_SCHEMAS_SET = {
|
DEFAULT_BLOCKED_SCHEMAS_SET = {
|
||||||
'syslog',
|
'syslog',
|
||||||
'dbus',
|
'dbus',
|
||||||
'kde',
|
'kde',
|
||||||
@@ -45,13 +45,22 @@ if __name__ == '__main__':
|
|||||||
services = json.loads(sys.argv[1])
|
services = json.loads(sys.argv[1])
|
||||||
title = sys.argv[2]
|
title = sys.argv[2]
|
||||||
body_by_format = json.loads(sys.argv[3])
|
body_by_format = json.loads(sys.argv[3])
|
||||||
|
schema_config = json.loads(sys.argv[4]) if len(sys.argv) > 4 else {}
|
||||||
|
|
||||||
|
allowed_schemas = set(schema_config.get('allowedSchemas', []))
|
||||||
|
custom_blocked = schema_config.get('blockedSchemas', [])
|
||||||
|
blocked_schemas = set(custom_blocked) if custom_blocked else DEFAULT_BLOCKED_SCHEMAS_SET
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for service in services:
|
for service in services:
|
||||||
url = service['url']
|
url = service['url']
|
||||||
schema = url.split(':')[0]
|
schema = url.split(':')[0]
|
||||||
|
|
||||||
if schema in BLOCKED_SCHEMAS_SET:
|
if allowed_schemas and schema not in allowed_schemas:
|
||||||
|
errors.append(f'[{schema}] Schema not in allowed list')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if schema in blocked_schemas:
|
||||||
errors.append(f'[{schema}] Blocked service schema')
|
errors.append(f'[{schema}] Blocked service schema')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user