Next (#890)
This commit is contained in:
commit
eb42029b5d
|
|
@ -7,14 +7,20 @@ updates:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "next"
|
target-branch: "next"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "3k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# npm updates for frontend - targeting "release/2k" branch
|
# npm updates for frontend - targeting "release/2k" branch (security updates only)
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/frontend"
|
directory: "/frontend"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "release/2k"
|
target-branch: "release/2k"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 0
|
||||||
|
labels:
|
||||||
|
- "2k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# npm updates for integration-tests - targeting "next" branch
|
# npm updates for integration-tests - targeting "next" branch
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
|
|
@ -23,14 +29,20 @@ updates:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "next"
|
target-branch: "next"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "3k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# npm updates for integration-tests - targeting "release/2k" branch
|
# npm updates for integration-tests - targeting "release/2k" branch (security updates only)
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/integration-tests"
|
directory: "/integration-tests"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "release/2k"
|
target-branch: "release/2k"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 0
|
||||||
|
labels:
|
||||||
|
- "2k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# Go modules updates for service - targeting "next" branch
|
# Go modules updates for service - targeting "next" branch
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
|
|
@ -39,14 +51,20 @@ updates:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "next"
|
target-branch: "next"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "3k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# Go modules updates for service - targeting "release/2k" branch
|
# Go modules updates for service - targeting "release/2k" branch (security updates only)
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/service"
|
directory: "/service"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "release/2k"
|
target-branch: "release/2k"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 0
|
||||||
|
labels:
|
||||||
|
- "2k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# Go modules updates for lang - targeting "next" branch
|
# Go modules updates for lang - targeting "next" branch
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
|
|
@ -55,14 +73,20 @@ updates:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "next"
|
target-branch: "next"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "3k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# Go modules updates for lang - targeting "release/2k" branch
|
# Go modules updates for lang - targeting "release/2k" branch (security updates only)
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/lang"
|
directory: "/lang"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "release/2k"
|
target-branch: "release/2k"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 0
|
||||||
|
labels:
|
||||||
|
- "2k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# Docker updates - targeting "next" branch
|
# Docker updates - targeting "next" branch
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
|
|
@ -71,12 +95,18 @@ updates:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "next"
|
target-branch: "next"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "3k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
# Docker updates - targeting "release/2k" branch
|
# Docker updates - targeting "release/2k" branch (security updates only)
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
target-branch: "release/2k"
|
target-branch: "release/2k"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 0
|
||||||
|
labels:
|
||||||
|
- "2k"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
|
|
|
||||||
29
SECURITY.md
29
SECURITY.md
|
|
@ -2,12 +2,35 @@
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Currently, only the `main` branch is "supported".
|
The following branches are currently being supported with security updates:
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| `main` | :white_check_mark: |
|
| `main` (3k release branch) | :white_check_mark: |
|
||||||
|
| `release/2k` (2k release branch) | :white_check_mark: |
|
||||||
|
|
||||||
|
To understand more about 2k vs 3k, see the following docs; https://docs.olivetin.app/upgrade/2k3k.html
|
||||||
|
|
||||||
|
## OliveTin *is* a remote code execution (RCE) "tool"
|
||||||
|
|
||||||
|
The very purpose of OliveTin is to allow users to execute commands remotely on a machine.
|
||||||
|
|
||||||
|
This means that, by design, OliveTin has much higher potential to be used for remote code execution (RCE), and any security vulnerabilities that do occur have the potential to be much more severe than in other types of software.
|
||||||
|
|
||||||
|
We hope that you understand that while the project goes to great aims to be safe, and mitigate, that security vulnerabilities are inevitable, as they are with all software of all sizes - like Kubernetes, the Kernel, etc - and OliveTin has substantially less resources than those projects.
|
||||||
|
|
||||||
|
With that being said, OliveTin tries to follow examples of best practice, so judge the project not on if/when it has security issues, but how security issues are responded to as the measure of quality.
|
||||||
|
|
||||||
|
This is why we take security very seriously, and why we encourage responsible disclosure practices when reporting vulnerabilities.
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please email `contact@jread.com` for responsible disclosure. Accepted issues will be made public once patched, and you will be given credit.
|
Please use responsible disclosure practices when reporting a vulnerability. **You will receive full credit for your discovery**, and we will work with you to ensure that the issue is resolved as quickly as **possible**. Please note that only James Read has access to security issues at the moment, so please be patient and understanding if you do not receive an immediate response.
|
||||||
|
|
||||||
|
* **Option A (preferred)**: GitHub Security Advisories, which allows you to report a vulnerability privately and securely. Use this direct link to report privately: `https://github.com/OliveTin/OliveTin/security/advisories/new`. This allows you to provide details without making them public.
|
||||||
|
|
||||||
|
* **Option B**: Please email `contact@jread.com` for responsible disclosure.
|
||||||
|
|
||||||
|
## Disclosure of how vulnerabilities were found
|
||||||
|
|
||||||
|
It is incredibly useful to not just patch security vulnerabilities, but also to understand how they were found. If you are able to share this information, it can help us and the community to better understand potential attack vectors and improve the overall security of the project.
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,14 @@ async function initClient () {
|
||||||
window.client = createClient(OliveTinApiService, transport)
|
window.client = createClient(OliveTinApiService, transport)
|
||||||
window.initResponse = await window.client.init({})
|
window.initResponse = await window.client.init({})
|
||||||
|
|
||||||
|
if (window.initResponse.enableCustomJs) {
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = '/custom-webui/custom.js'
|
||||||
|
script.async = true
|
||||||
|
script.id = 'olivetin-custom-js'
|
||||||
|
document.head.appendChild(script)
|
||||||
|
}
|
||||||
|
|
||||||
const i18nSettings = createI18n({
|
const i18nSettings = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: getSelectedLanguage(),
|
locale: getSelectedLanguage(),
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,13 @@ export declare type GetLogsRequest = Message<"olivetin.api.v1.GetLogsRequest"> &
|
||||||
* @generated from field: string date_filter = 2;
|
* @generated from field: string date_filter = 2;
|
||||||
*/
|
*/
|
||||||
dateFilter: string;
|
dateFilter: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of logs per page (optional; server default used if 0 or unset)
|
||||||
|
*
|
||||||
|
* @generated from field: int64 page_size = 3;
|
||||||
|
*/
|
||||||
|
pageSize: bigint;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1846,3 +1853,4 @@ export declare const OliveTinApiService: GenService<{
|
||||||
output: typeof EntitySchema;
|
output: typeof EntitySchema;
|
||||||
},
|
},
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -231,6 +231,7 @@ function updateHeaderFromInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyleMods()
|
applyStyleMods()
|
||||||
|
loadCustomJsIfEnabled()
|
||||||
|
|
||||||
renderNavigation()
|
renderNavigation()
|
||||||
applyTheme()
|
applyTheme()
|
||||||
|
|
@ -369,6 +370,17 @@ function applyTheme() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadCustomJsIfEnabled() {
|
||||||
|
if (!window.initResponse?.enableCustomJs || document.getElementById('olivetin-custom-js')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = '/custom-webui/custom.js'
|
||||||
|
script.async = true
|
||||||
|
script.id = 'olivetin-custom-js'
|
||||||
|
document.head.appendChild(script)
|
||||||
|
}
|
||||||
|
|
||||||
function applyStyleMods() {
|
function applyStyleMods() {
|
||||||
if (!window.initResponse || !window.initResponse.styleMods) {
|
if (!window.initResponse || !window.initResponse.styleMods) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" @page-change="handlePageChange" class="padding"
|
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class="padding"
|
||||||
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ const confirmationChecked = ref(false)
|
||||||
const hasConfirmation = ref(false)
|
const hasConfirmation = ref(false)
|
||||||
const formErrors = ref({})
|
const formErrors = ref({})
|
||||||
const actionArguments = ref([])
|
const actionArguments = ref([])
|
||||||
|
const popupOnStart = ref('')
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
|
|
||||||
|
|
@ -93,6 +94,7 @@ async function setup() {
|
||||||
|
|
||||||
title.value = action.title
|
title.value = action.title
|
||||||
icon.value = action.icon
|
icon.value = action.icon
|
||||||
|
popupOnStart.value = action.popupOnStart || ''
|
||||||
actionArguments.value = action.arguments || []
|
actionArguments.value = action.arguments || []
|
||||||
argValues.value = {}
|
argValues.value = {}
|
||||||
formErrors.value = {}
|
formErrors.value = {}
|
||||||
|
|
@ -418,7 +420,11 @@ async function handleSubmit(event) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await startAction(argvs)
|
const response = await startAction(argvs)
|
||||||
router.push(`/logs/${response.executionTrackingId}`)
|
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||||
|
router.push(`/logs/${response.executionTrackingId}`)
|
||||||
|
} else {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to start action:', err)
|
console.error('Failed to start action:', err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" @page-change="handlePageChange" class = "padding"
|
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" :page="currentPage" @page-change="handlePageChange" class = "padding"
|
||||||
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -150,6 +150,7 @@ async function fetchLogs() {
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
"startOffset": BigInt(startOffset),
|
"startOffset": BigInt(startOffset),
|
||||||
|
"pageSize": BigInt(pageSize.value),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add date filter if selected
|
// Add date filter if selected
|
||||||
|
|
@ -160,7 +161,6 @@ async function fetchLogs() {
|
||||||
const response = await window.client.getLogs(args)
|
const response = await window.client.getLogs(args)
|
||||||
|
|
||||||
logs.value = response.logs
|
logs.value = response.logs
|
||||||
pageSize.value = Number(response.pageSize) || 0
|
|
||||||
totalCount.value = Number(response.totalCount) || 0
|
totalCount.value = Number(response.totalCount) || 0
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch logs:', err)
|
console.error('Failed to fetch logs:', err)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wait-on": "^9.0.3"
|
"wait-on": "^9.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^6.2.2",
|
"chai": "^6.2.2",
|
||||||
|
|
@ -209,9 +209,9 @@
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/@hapi/tlds": {
|
"node_modules/@hapi/tlds": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.5.tgz",
|
||||||
"integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==",
|
"integrity": "sha512-Vq/1gnIIsvFUpKlDdfrPd/ssHDpAyBP/baVukh3u2KSG2xoNjsnRNjQiPmuyPPGqsn1cqVWWhtZHfOBaLizFRQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|
@ -321,9 +321,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
|
|
@ -1437,9 +1437,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/joi": {
|
"node_modules/joi": {
|
||||||
"version": "18.0.1",
|
"version": "18.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz",
|
||||||
"integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==",
|
"integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/address": "^5.1.1",
|
"@hapi/address": "^5.1.1",
|
||||||
|
|
@ -1547,9 +1547,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
|
|
@ -2210,14 +2211,14 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/wait-on": {
|
"node_modules/wait-on": {
|
||||||
"version": "9.0.3",
|
"version": "9.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
|
||||||
"integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==",
|
"integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.5",
|
||||||
"joi": "^18.0.1",
|
"joi": "^18.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.23",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"rxjs": "^7.8.2"
|
"rxjs": "^7.8.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,6 @@
|
||||||
"selenium-webdriver": "^4.40.0"
|
"selenium-webdriver": "^4.40.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wait-on": "^9.0.3"
|
"wait-on": "^9.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
logLevel: "DEBUG"
|
logLevel: "DEBUG"
|
||||||
checkForUpdates: false
|
checkForUpdates: false
|
||||||
|
defaultPopupOnStart: execution-dialog
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
- title: Test checkbox argument
|
- title: Test checkbox argument
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
logLevel: "DEBUG"
|
logLevel: "DEBUG"
|
||||||
checkForUpdates: false
|
checkForUpdates: false
|
||||||
|
defaultPopupOnStart: execution-dialog
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
- title: Test datetime argument
|
- title: Test datetime argument
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
logLevel: "DEBUG"
|
logLevel: "DEBUG"
|
||||||
checkForUpdates: false
|
checkForUpdates: false
|
||||||
|
defaultPopupOnStart: execution-dialog
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
- title: Test suggestionsBrowserKey
|
- title: Test suggestionsBrowserKey
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ message StartActionByGetAndWaitResponse {
|
||||||
message GetLogsRequest{
|
message GetLogsRequest{
|
||||||
int64 start_offset = 1;
|
int64 start_offset = 1;
|
||||||
string date_filter = 2; // Optional date filter in YYYY-MM-DD format
|
string date_filter = 2; // Optional date filter in YYYY-MM-DD format
|
||||||
|
int64 page_size = 3; // Number of logs per page (optional; server default used if 0 or unset)
|
||||||
};
|
};
|
||||||
|
|
||||||
message LogEntry {
|
message LogEntry {
|
||||||
|
|
|
||||||
|
|
@ -1105,6 +1105,7 @@ type GetLogsRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
StartOffset int64 `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
|
StartOffset int64 `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
|
||||||
DateFilter string `protobuf:"bytes,2,opt,name=date_filter,json=dateFilter,proto3" json:"date_filter,omitempty"` // Optional date filter in YYYY-MM-DD format
|
DateFilter string `protobuf:"bytes,2,opt,name=date_filter,json=dateFilter,proto3" json:"date_filter,omitempty"` // Optional date filter in YYYY-MM-DD format
|
||||||
|
PageSize int64 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` // Number of logs per page (optional; server default used if 0 or unset)
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -1153,6 +1154,13 @@ func (x *GetLogsRequest) GetDateFilter() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *GetLogsRequest) GetPageSize() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.PageSize
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type LogEntry struct {
|
type LogEntry struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
DatetimeStarted string `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
|
DatetimeStarted string `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
|
||||||
|
|
@ -3972,11 +3980,12 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\x1eStartActionByGetAndWaitRequest\x12\x1b\n" +
|
"\x1eStartActionByGetAndWaitRequest\x12\x1b\n" +
|
||||||
"\taction_id\x18\x01 \x01(\tR\bactionId\"Y\n" +
|
"\taction_id\x18\x01 \x01(\tR\bactionId\"Y\n" +
|
||||||
"\x1fStartActionByGetAndWaitResponse\x126\n" +
|
"\x1fStartActionByGetAndWaitResponse\x126\n" +
|
||||||
"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"T\n" +
|
"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"q\n" +
|
||||||
"\x0eGetLogsRequest\x12!\n" +
|
"\x0eGetLogsRequest\x12!\n" +
|
||||||
"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" +
|
"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" +
|
||||||
"\vdate_filter\x18\x02 \x01(\tR\n" +
|
"\vdate_filter\x18\x02 \x01(\tR\n" +
|
||||||
"dateFilter\"\x89\x05\n" +
|
"dateFilter\x12\x1b\n" +
|
||||||
|
"\tpage_size\x18\x03 \x01(\x03R\bpageSize\"\x89\x05\n" +
|
||||||
"\bLogEntry\x12)\n" +
|
"\bLogEntry\x12)\n" +
|
||||||
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
|
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
|
||||||
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
|
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,19 @@ func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest,
|
||||||
return connect.NewResponse(res), nil
|
return connect.NewResponse(res), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveLogsPageSize(requestPageSize, defaultPageSize int64) int64 {
|
||||||
|
if requestPageSize == 0 {
|
||||||
|
return defaultPageSize
|
||||||
|
}
|
||||||
|
if requestPageSize < 10 {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
if requestPageSize > 100 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return requestPageSize
|
||||||
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
|
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
|
||||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
|
|
@ -501,12 +514,9 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetL
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageSize := resolveLogsPageSize(req.Msg.GetPageSize(), api.cfg.LogHistoryPageSize)
|
||||||
|
logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, pageSize, req.Msg.DateFilter)
|
||||||
ret := &apiv1.GetLogsResponse{}
|
ret := &apiv1.GetLogsResponse{}
|
||||||
dateFilter := ""
|
|
||||||
if req.Msg.DateFilter != "" {
|
|
||||||
dateFilter = req.Msg.DateFilter
|
|
||||||
}
|
|
||||||
logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, api.cfg.LogHistoryPageSize, dateFilter)
|
|
||||||
for _, le := range logEntries {
|
for _, le := range logEntries {
|
||||||
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
|
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent
|
||||||
for _, subitem := range contents {
|
for _, subitem := range contents {
|
||||||
c := cloneItem(subitem, ent, entityType, rr)
|
c := cloneItem(subitem, ent, entityType, rr)
|
||||||
|
|
||||||
log.Infof("cloneItem: %+v", c)
|
log.Tracef("cloneItem: %+v", c)
|
||||||
|
|
||||||
if c != nil {
|
if c != nil {
|
||||||
ret = append(ret, c)
|
ret = append(ret, c)
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,7 @@ func checkShellArgumentSafety(action *config.Action) error {
|
||||||
if action.Shell == "" {
|
if action.Shell == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
unsafe := map[string]struct{}{"url": {}, "email": {}, "raw_string_multiline": {}, "very_dangerous_raw_string": {}}
|
unsafe := map[string]struct{}{"url": {}, "email": {}, "raw_string_multiline": {}, "very_dangerous_raw_string": {}, "password": {}}
|
||||||
for _, arg := range action.Arguments {
|
for _, arg := range action.Arguments {
|
||||||
if _, bad := unsafe[arg.Type]; bad {
|
if _, bad := unsafe[arg.Type]; bad {
|
||||||
return fmt.Errorf("unsafe argument type '%s' cannot be used with Shell execution. Use 'exec' instead. See https://docs.olivetin.app/action_execution/shellvsexec.html", arg.Type)
|
return fmt.Errorf("unsafe argument type '%s' cannot be used with Shell execution. Use 'exec' instead. See https://docs.olivetin.app/action_execution/shellvsexec.html", arg.Type)
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,40 @@ func TestCheckShellArgumentSafetyWithSafeTypes(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckShellArgumentSafetyWithPassword(t *testing.T) {
|
||||||
|
a1 := config.Action{
|
||||||
|
Title: "Auth with password",
|
||||||
|
Shell: "somecommand --password '{{password}}'",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "password",
|
||||||
|
Type: "password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := checkShellArgumentSafety(&a1)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unsafe argument type 'password' cannot be used with Shell execution")
|
||||||
|
assert.Contains(t, err.Error(), "https://docs.olivetin.app/action_execution/shellvsexec.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckShellArgumentSafetyWithPasswordAndExec(t *testing.T) {
|
||||||
|
a1 := config.Action{
|
||||||
|
Title: "Auth with password via exec",
|
||||||
|
Exec: []string{"somecommand", "--password", "{{password}}"},
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "password",
|
||||||
|
Type: "password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := checkShellArgumentSafety(&a1)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestTypeSafetyCheckUrl(t *testing.T) {
|
func TestTypeSafetyCheckUrl(t *testing.T) {
|
||||||
assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
|
assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
|
||||||
assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
|
assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
|
||||||
|
|
|
||||||
|
|
@ -664,6 +664,7 @@ func stepParseArgs(req *ExecutionRequest) bool {
|
||||||
return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
|
return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterToDefinedArgumentsOnly(req)
|
||||||
mangleInvalidArgumentValues(req)
|
mangleInvalidArgumentValues(req)
|
||||||
|
|
||||||
if hasExec(req) {
|
if hasExec(req) {
|
||||||
|
|
@ -686,6 +687,9 @@ func handleExecBranch(req *ExecutionRequest) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleShellBranch(req *ExecutionRequest) bool {
|
func handleShellBranch(req *ExecutionRequest) bool {
|
||||||
|
if hasWebhookTag(req) {
|
||||||
|
return fail(req, fmt.Errorf("webhooks cannot use Shell execution; use exec instead. See https://docs.olivetin.app/action_execution/shellvsexec.html"))
|
||||||
|
}
|
||||||
if err := checkShellArgumentSafety(req.Binding.Action); err != nil {
|
if err := checkShellArgumentSafety(req.Binding.Action); err != nil {
|
||||||
return fail(req, err)
|
return fail(req, err)
|
||||||
}
|
}
|
||||||
|
|
@ -707,6 +711,34 @@ func ensureArgumentMap(req *ExecutionRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterToDefinedArgumentsOnly(req *ExecutionRequest) {
|
||||||
|
definedNames := make(map[string]struct{})
|
||||||
|
for _, arg := range req.Binding.Action.Arguments {
|
||||||
|
definedNames[arg.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
filtered := make(map[string]string)
|
||||||
|
for k, v := range req.Arguments {
|
||||||
|
if keepArgument(k, definedNames) {
|
||||||
|
filtered[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.Arguments = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func keepArgument(name string, definedNames map[string]struct{}) bool {
|
||||||
|
_, ok := definedNames[name]
|
||||||
|
return ok || strings.HasPrefix(name, "ot_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasWebhookTag(req *ExecutionRequest) bool {
|
||||||
|
for _, tag := range req.Tags {
|
||||||
|
if tag == "webhook" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func injectSystemArgs(req *ExecutionRequest) {
|
func injectSystemArgs(req *ExecutionRequest) {
|
||||||
req.Arguments["ot_executionTrackingId"] = req.TrackingID
|
req.Arguments["ot_executionTrackingId"] = req.TrackingID
|
||||||
req.Arguments["ot_username"] = req.AuthenticatedUser.Username
|
req.Arguments["ot_username"] = req.AuthenticatedUser.Username
|
||||||
|
|
|
||||||
|
|
@ -295,3 +295,103 @@ func TestMangleInvalidArgumentValues(t *testing.T) {
|
||||||
assert.Equal(t, req.logEntry.Output, "The date is: 1990-01-10T12:00:00\n", "Date should be mangled to a valid format")
|
assert.Equal(t, req.logEntry.Output, "The date is: 1990-01-10T12:00:00\n", "Date should be mangled to a valid format")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebhookRejectsShellExecution(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
e := DefaultExecutor(cfg)
|
||||||
|
a1 := &config.Action{
|
||||||
|
Title: "Webhook Shell Reject",
|
||||||
|
Shell: "echo '{{ msg }}'",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{Name: "msg", Type: "ascii"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cfg.Actions = append(cfg.Actions, a1)
|
||||||
|
cfg.Sanitize()
|
||||||
|
e.RebuildActionMap()
|
||||||
|
|
||||||
|
req := ExecutionRequest{
|
||||||
|
Tags: []string{"webhook"},
|
||||||
|
AuthenticatedUser: auth.UserFromSystem(cfg, "webhook"),
|
||||||
|
Cfg: cfg,
|
||||||
|
Arguments: map[string]string{"msg": "hello"},
|
||||||
|
Binding: e.FindBindingWithNoEntity(a1),
|
||||||
|
}
|
||||||
|
|
||||||
|
wg, _ := e.ExecRequest(&req)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.NotNil(t, req.logEntry)
|
||||||
|
assert.Equal(t, int32(-1337), req.logEntry.ExitCode)
|
||||||
|
assert.Contains(t, req.logEntry.Output, "webhooks cannot use Shell execution")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhookAllowsExecExecution(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
e := DefaultExecutor(cfg)
|
||||||
|
a1 := &config.Action{
|
||||||
|
Title: "Webhook Exec OK",
|
||||||
|
Exec: []string{"echo", "{{ msg }}"},
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{Name: "msg", Type: "ascii"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cfg.Actions = append(cfg.Actions, a1)
|
||||||
|
cfg.Sanitize()
|
||||||
|
e.RebuildActionMap()
|
||||||
|
|
||||||
|
req := ExecutionRequest{
|
||||||
|
Tags: []string{"webhook"},
|
||||||
|
AuthenticatedUser: auth.UserFromSystem(cfg, "webhook"),
|
||||||
|
Cfg: cfg,
|
||||||
|
Arguments: map[string]string{"msg": "hello"},
|
||||||
|
Binding: e.FindBindingWithNoEntity(a1),
|
||||||
|
}
|
||||||
|
|
||||||
|
wg, _ := e.ExecRequest(&req)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.NotNil(t, req.logEntry)
|
||||||
|
assert.Equal(t, int32(0), req.logEntry.ExitCode)
|
||||||
|
assert.Contains(t, req.logEntry.Output, "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterToDefinedArgumentsOnly(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Filter test",
|
||||||
|
Shell: "echo '{{ name }}'",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{Name: "name", Type: "ascii"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Arguments = map[string]string{
|
||||||
|
"name": "Alice",
|
||||||
|
"webhook_path": "/malicious/$(id)",
|
||||||
|
"extra_undefined": "ignored",
|
||||||
|
}
|
||||||
|
|
||||||
|
filterToDefinedArgumentsOnly(req)
|
||||||
|
|
||||||
|
assert.Equal(t, "Alice", req.Arguments["name"])
|
||||||
|
assert.Empty(t, req.Arguments["webhook_path"])
|
||||||
|
assert.Empty(t, req.Arguments["extra_undefined"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Filter test",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
req.Arguments = map[string]string{
|
||||||
|
"ot_executionTrackingId": "track-123",
|
||||||
|
"ot_username": "webhook",
|
||||||
|
}
|
||||||
|
|
||||||
|
filterToDefinedArgumentsOnly(req)
|
||||||
|
|
||||||
|
assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"])
|
||||||
|
assert.Equal(t, "webhook", req.Arguments["ot_username"])
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package tpl
|
package tpl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -12,8 +13,20 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func jsonFunc(v any) (string, error) {
|
||||||
|
if v == nil {
|
||||||
|
return "null", nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
var tpl = template.New("tpl").
|
var tpl = template.New("tpl").
|
||||||
Option("missingkey=error")
|
Option("missingkey=error").
|
||||||
|
Funcs(template.FuncMap{"Json": jsonFunc})
|
||||||
|
|
||||||
type olivetinInfo struct {
|
type olivetinInfo struct {
|
||||||
Build *installationinfo.BuildInfo
|
Build *installationinfo.BuildInfo
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package tpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseTemplateWithActionContext_Json(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
ent *entities.Entity
|
||||||
|
args map[string]string
|
||||||
|
expectedOutput string
|
||||||
|
expectError bool
|
||||||
|
checkJsonOnly bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Arguments piped to Json",
|
||||||
|
source: `echo {{ .Arguments | Json }}`,
|
||||||
|
ent: nil,
|
||||||
|
args: map[string]string{"value": "true", "ot_username": "alice"},
|
||||||
|
expectedOutput: `echo `,
|
||||||
|
expectError: false,
|
||||||
|
checkJsonOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CurrentEntity field piped to Json",
|
||||||
|
source: `curl -d {{ .CurrentEntity.foo.bar | Json }}`,
|
||||||
|
ent: &entities.Entity{Data: map[string]any{"foo": map[string]any{"bar": "baz"}}},
|
||||||
|
args: nil,
|
||||||
|
expectedOutput: `curl -d "baz"`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CurrentEntity nested object piped to Json",
|
||||||
|
source: `curl --json -d {{ .CurrentEntity.payload | Json }}`,
|
||||||
|
ent: &entities.Entity{Data: map[string]any{"payload": map[string]any{"on": true}}},
|
||||||
|
args: nil,
|
||||||
|
expectedOutput: `curl --json -d {"on":true}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single argument value as Json",
|
||||||
|
source: `echo {{ .Arguments.value | Json }}`,
|
||||||
|
ent: nil,
|
||||||
|
args: map[string]string{"value": "hello"},
|
||||||
|
expectedOutput: `echo "hello"`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := ParseTemplateWithActionContext(tt.source, tt.ent, tt.args)
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if tt.checkJsonOnly {
|
||||||
|
assertJsonOutput(t, output, tt.expectedOutput, tt.args)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.expectedOutput, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertJsonOutput(t *testing.T, output, expectedPrefix string, args map[string]string) {
|
||||||
|
t.Helper()
|
||||||
|
prefix := strings.TrimSuffix(expectedPrefix, " ")
|
||||||
|
assert.True(t, strings.HasPrefix(output, prefix), "output %q should start with %q", output, prefix)
|
||||||
|
jsonPart := strings.TrimPrefix(output, prefix)
|
||||||
|
jsonPart = strings.TrimSpace(jsonPart)
|
||||||
|
var decoded map[string]string
|
||||||
|
err := json.Unmarshal([]byte(jsonPart), &decoded)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for k, v := range args {
|
||||||
|
assert.Equal(t, v, decoded[k], "decoded JSON should contain %s=%s", k, v)
|
||||||
|
}
|
||||||
|
assert.Len(t, decoded, len(args))
|
||||||
|
}
|
||||||
|
|
@ -150,13 +150,28 @@ func (h *WebhookHandler) executeAction(action *config.Action, args map[string]st
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
definedArgs := filterToDefinedArguments(args, action)
|
||||||
req := &executor.ExecutionRequest{
|
req := &executor.ExecutionRequest{
|
||||||
Binding: binding,
|
Binding: binding,
|
||||||
Cfg: h.cfg,
|
Cfg: h.cfg,
|
||||||
Tags: []string{"webhook"},
|
Tags: []string{"webhook"},
|
||||||
Arguments: args,
|
Arguments: definedArgs,
|
||||||
AuthenticatedUser: auth.UserFromSystem(h.cfg, "webhook"),
|
AuthenticatedUser: auth.UserFromSystem(h.cfg, "webhook"),
|
||||||
}
|
}
|
||||||
|
|
||||||
h.executor.ExecRequest(req)
|
h.executor.ExecRequest(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterToDefinedArguments(args map[string]string, action *config.Action) map[string]string {
|
||||||
|
definedNames := make(map[string]struct{})
|
||||||
|
for _, arg := range action.Arguments {
|
||||||
|
definedNames[arg.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
filtered := make(map[string]string)
|
||||||
|
for k, v := range args {
|
||||||
|
if _, ok := definedNames[k]; ok {
|
||||||
|
filtered[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package webhooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilterToDefinedArguments(t *testing.T) {
|
||||||
|
action := &config.Action{
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{Name: "repo", Type: "ascii_identifier"},
|
||||||
|
{Name: "branch", Type: "ascii_identifier"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
args := map[string]string{
|
||||||
|
"repo": "my-repo",
|
||||||
|
"branch": "main",
|
||||||
|
"webhook_path": "/deploy/prod",
|
||||||
|
"webhook_header_x_custom": "malicious",
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := filterToDefinedArguments(args, action)
|
||||||
|
|
||||||
|
assert.Equal(t, "my-repo", filtered["repo"])
|
||||||
|
assert.Equal(t, "main", filtered["branch"])
|
||||||
|
assert.Empty(t, filtered["webhook_path"])
|
||||||
|
assert.Empty(t, filtered["webhook_header_x_custom"])
|
||||||
|
assert.Len(t, filtered, 2)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RELEASE_NAME="${1:-}"
|
||||||
|
GHCR_IMAGE="ghcr.io/olivetin/olivetin"
|
||||||
|
DOCKERHUB_IMAGE="jamesread/olivetin"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[unrelease] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_confirm() {
|
||||||
|
local prompt="$1"
|
||||||
|
local default="${2:-n}"
|
||||||
|
if [[ "$default" == "y" ]]; then
|
||||||
|
read -r -p "$prompt [Y/n] " reply
|
||||||
|
else
|
||||||
|
read -r -p "$prompt [y/N] " reply
|
||||||
|
fi
|
||||||
|
reply="${reply:-$default}"
|
||||||
|
case "$(echo "$reply" | tr '[:upper:]' '[:lower:]')" in
|
||||||
|
y|yes) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$RELEASE_NAME" ]]; then
|
||||||
|
echo "Usage: $0 <release_name>" >&2
|
||||||
|
echo "Example: $0 3000.10.0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Release to remove: $RELEASE_NAME"
|
||||||
|
log "This will: 1) Delete GitHub release, 2) Delete GitHub tag, 3) Delete GHCR image tag, 4) Delete Docker Hub image tag"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- GitHub release ---
|
||||||
|
log "Step 1: Delete GitHub release '$RELEASE_NAME'"
|
||||||
|
if prompt_confirm "Delete GitHub release?" "n"; then
|
||||||
|
if err=$(gh release delete "$RELEASE_NAME" --yes 2>&1); then
|
||||||
|
log "Deleted GitHub release."
|
||||||
|
else
|
||||||
|
log "Failed to delete GitHub release:" >&2
|
||||||
|
echo "$err" | sed 's/^/[unrelease] /' >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Skipped GitHub release."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- GitHub tag ---
|
||||||
|
log "Step 2: Delete GitHub tag '$RELEASE_NAME'"
|
||||||
|
if prompt_confirm "Delete GitHub tag?" "n"; then
|
||||||
|
repo=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null) || repo="olivetin/olivetin"
|
||||||
|
if err=$(gh api -X DELETE "repos/$repo/git/refs/tags/$RELEASE_NAME" 2>&1); then
|
||||||
|
log "Deleted GitHub tag."
|
||||||
|
else
|
||||||
|
log "Failed to delete GitHub tag:" >&2
|
||||||
|
echo "$err" | sed 's/^/[unrelease] /' >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Skipped GitHub tag."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- GHCR ---
|
||||||
|
log "Step 3: Delete GHCR image tag $GHCR_IMAGE:$RELEASE_NAME"
|
||||||
|
if prompt_confirm "Delete GHCR container image version?" "n"; then
|
||||||
|
list_err=$(gh api "orgs/olivetin/packages/container/olivetin/versions" --jq ".[] | select(.metadata.container.tags[]? == \"$RELEASE_NAME\") | .id" 2>&1) || true
|
||||||
|
version_id=$(echo "$list_err" | head -1)
|
||||||
|
if [[ -z "$version_id" || ! "$version_id" =~ ^[0-9]+$ ]]; then
|
||||||
|
log "Could not resolve GHCR version for tag '$RELEASE_NAME' (need read:packages scope, or tag may not exist)." >&2
|
||||||
|
if [[ "$list_err" == *"message"* ]]; then
|
||||||
|
msg=$(echo "$list_err" | sed -n 's/.*"message":"\([^"]*\)".*/\1/p' | head -1)
|
||||||
|
[[ -n "$msg" ]] && log " $msg" >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if err=$(gh api -X DELETE "orgs/olivetin/packages/container/olivetin/versions/$version_id" 2>&1); then
|
||||||
|
log "Deleted GHCR version (id: $version_id)."
|
||||||
|
else
|
||||||
|
log "Failed to delete GHCR version:" >&2
|
||||||
|
echo "$err" | sed 's/^/[unrelease] /' >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Skipped GHCR."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Docker Hub ---
|
||||||
|
log "Step 4: Delete Docker Hub image tag $DOCKERHUB_IMAGE:$RELEASE_NAME"
|
||||||
|
if prompt_confirm "Delete Docker Hub image tag? (requires DOCKERHUB_TOKEN)" "n"; then
|
||||||
|
if [[ -z "${DOCKERHUB_TOKEN:-}" ]]; then
|
||||||
|
log "DOCKERHUB_TOKEN is not set. Get a token from https://hub.docker.com/settings/security and run: DOCKERHUB_TOKEN=xxx $0 $RELEASE_NAME" >&2
|
||||||
|
log "Skipped Docker Hub."
|
||||||
|
else
|
||||||
|
status=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: Bearer $DOCKERHUB_TOKEN" \
|
||||||
|
"https://hub.docker.com/v2/repositories/$DOCKERHUB_IMAGE/tags/$RELEASE_NAME/")
|
||||||
|
if [[ "$status" == "204" ]]; then
|
||||||
|
log "Deleted Docker Hub tag."
|
||||||
|
else
|
||||||
|
log "Docker Hub delete returned HTTP $status (tag may not exist or token invalid)." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Skipped Docker Hub."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Done."
|
||||||
Loading…
Reference in New Issue