Next (#766)
This commit is contained in:
commit
f28944bee2
|
|
@ -63,7 +63,10 @@ changelog:
|
|||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^cicd:'
|
||||
- '^chore:'
|
||||
- '^release:'
|
||||
- '^refactor:'
|
||||
- '^Merge branch'
|
||||
|
||||
archives:
|
||||
- formats: tar.gz
|
||||
|
|
|
|||
|
|
@ -35,14 +35,14 @@ The preferred way to communicate is probably via Discord or GitHub issues.
|
|||
```
|
||||
# Step1: setup compile env
|
||||
# - Fedora
|
||||
dnf install git go protobuf-compiler make -y
|
||||
dnf install git go protobuf-compiler make pre-commit -y
|
||||
# - Windows with chocolatey
|
||||
choco install git go protoc make python nodejs-lts -y
|
||||
choco install git go protoc make python nodejs-lts -y && pip install pre-commit
|
||||
|
||||
# Step2: clone and setup repo
|
||||
git clone https://github.com/OliveTin/OliveTin.git
|
||||
cd OliveTin
|
||||
make githooks
|
||||
pre-commit install
|
||||
|
||||
# Step3: compile binary for current dev env (OS, ARCH)
|
||||
# `make proto` will also run `make go-tools`, which installs "buf". This binary
|
||||
|
|
|
|||
3
Makefile
3
Makefile
|
|
@ -56,4 +56,7 @@ clean:
|
|||
$(call delete-files,reports)
|
||||
$(call delete-files,gen)
|
||||
|
||||
config-tool:
|
||||
cd service && go run cmd/config-tool/main.go
|
||||
|
||||
.PHONY: proto service
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@ import { initWebsocket } from './js/websocket.js'
|
|||
import combinedTranslations from '../lang/combined_output.json'
|
||||
|
||||
function getSelectedLanguage () {
|
||||
const storedLanguage = localStorage.getItem('olivetin-language');
|
||||
const storedLanguage = localStorage.getItem('olivetin-language')
|
||||
|
||||
if (storedLanguage && storedLanguage !== 'auto') {
|
||||
return storedLanguage;
|
||||
return storedLanguage
|
||||
}
|
||||
|
||||
if (storedLanguage === 'auto') {
|
||||
localStorage.removeItem('olivetin-language');
|
||||
localStorage.removeItem('olivetin-language')
|
||||
}
|
||||
|
||||
if (navigator.languages && navigator.languages.length > 0) {
|
||||
|
|
@ -50,7 +50,7 @@ function getSelectedLanguage() {
|
|||
}
|
||||
}
|
||||
|
||||
return 'en';
|
||||
return 'en'
|
||||
}
|
||||
|
||||
async function initClient () {
|
||||
|
|
|
|||
|
|
@ -9,24 +9,24 @@
|
|||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@connectrpc/connect": "^2.1.0",
|
||||
"@connectrpc/connect-web": "^2.1.0",
|
||||
"@hugeicons/core-free-icons": "^1.2.1",
|
||||
"@connectrpc/connect": "^2.1.1",
|
||||
"@connectrpc/connect-web": "^2.1.1",
|
||||
"@hugeicons/core-free-icons": "^2.0.0",
|
||||
"@hugeicons/vue": "^1.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.8.7",
|
||||
"picocrank": "^1.8.9",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.2.2",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vite": "^7.2.4",
|
||||
"vue-i18n": "^11.2.1",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint": "^16.26.0",
|
||||
"stylelint-config-standard": "^39.0.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -178,23 +178,84 @@
|
|||
"license": "(Apache-2.0 AND BSD-3-Clause)",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@cacheable/memory": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.5.tgz",
|
||||
"integrity": "sha512-fkiAxCvssEyJZ5fxX4tcdZFRmW9JehSTGvvqmXn6rTzG5cH6V/3C4ad8yb01vOjp2xBydHkHrgpW0qeGtzt6VQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cacheable/utils": "^2.3.0",
|
||||
"@keyv/bigmap": "^1.1.0",
|
||||
"hookified": "^1.12.2",
|
||||
"keyv": "^5.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.0.tgz",
|
||||
"integrity": "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hashery": "^1.2.0",
|
||||
"hookified": "^1.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"keyv": "^5.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@cacheable/memory/node_modules/keyv": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz",
|
||||
"integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@keyv/serialize": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cacheable/utils": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.1.tgz",
|
||||
"integrity": "sha512-38NJXjIr4W1Sghun8ju+uYWD8h2c61B4dKwfnQHVDFpAJ9oS28RpfqZQJ6Dgd3RceGkILDY9YT+72HJR3LoeSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hashery": "^1.2.0",
|
||||
"keyv": "^5.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@cacheable/utils/node_modules/keyv": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz",
|
||||
"integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@keyv/serialize": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.0.tgz",
|
||||
"integrity": "sha512-xhiwnYlJNHzmFsRw+iSPIwXR/xweTvTw8x5HiwWp10sbVtd4OpOXbRgE7V58xs1EC17fzusF1f5uOAy24OkBuA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.1.tgz",
|
||||
"integrity": "sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@connectrpc/connect-web": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-2.1.0.tgz",
|
||||
"integrity": "sha512-4IBFeMeXS1RVtmmFE/MwH+vWq/5vDRKys70va+DAaWDh83Rdy0iUQOJbITUDzvonlY5as3vwfs5yy9Yp2miHSw==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-2.1.1.tgz",
|
||||
"integrity": "sha512-J8317Q2MaFRCT1jzVR1o06bZhDIBmU0UAzWx6xOIXzOq8+k71/+k7MUF7AwcBUX+34WIvbm5syRgC5HXQA8fOg==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.7.0",
|
||||
"@connectrpc/connect": "2.1.0"
|
||||
"@connectrpc/connect": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
|
|
@ -374,9 +435,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@hugeicons/core-free-icons": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-1.2.1.tgz",
|
||||
"integrity": "sha512-ho0QdGMkgL+kt+QsZocCsKvJou1rfyVQWARrxIhNLi+9tCKayUUtD9jlHgioaRphmskSl84TxrDm9Ae0G4Uu1g==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-2.0.0.tgz",
|
||||
"integrity": "sha512-OSfv5k0iB0yG61dcfK7jcf00AIK8EXyQOgtcNJzSBFvm88n9VOelkxihZHJnNwDUFpO/jZI3vZSVp6i1dmRvJQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hugeicons/vue": {
|
||||
|
|
@ -429,13 +490,13 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
|
||||
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.1.tgz",
|
||||
"integrity": "sha512-2V1A4yaN9ElAnQ6ih3HHEc+jZ+sHV6BlQHjCsnIVlOotL5NCUgJElIxgUFiJs6zV4puoAq3hHuQIfWNp+J+8yQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.1.12",
|
||||
"@intlify/shared": "11.1.12"
|
||||
"@intlify/message-compiler": "11.2.1",
|
||||
"@intlify/shared": "11.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
|
|
@ -445,12 +506,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
|
||||
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.1.tgz",
|
||||
"integrity": "sha512-J2454D3Agg3Kvgaj14gxTleJU8/H06Sisz7C2BwiHF0/i5Soyfb5ySpwn8GCL6yscDbOGj6xM+lUe6gO6BFQyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.1.12",
|
||||
"@intlify/shared": "11.2.1",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -461,9 +522,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
|
||||
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.1.tgz",
|
||||
"integrity": "sha512-O67LZM4dbfr70WCsZLW+g+pIXdgQ66laLVd/FicW7iYgP/RuH0X1FDGSh+Hr9Gou/8TeldUE6KmTGdLwX2ufIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
|
|
@ -518,9 +579,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@keyv/serialize": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.0.tgz",
|
||||
"integrity": "sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
|
||||
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -557,9 +618,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.29",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
|
||||
"integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==",
|
||||
"version": "1.0.0-beta.50",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",
|
||||
"integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
|
|
@ -613,12 +674,12 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
"integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-beta.29"
|
||||
"@rolldown/pluginutils": "1.0.0-beta.50"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
|
|
@ -1056,24 +1117,27 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cacheable": {
|
||||
"version": "1.10.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.4.tgz",
|
||||
"integrity": "sha512-Gd7ccIUkZ9TE2odLQVS+PDjIvQCdJKUlLdJRVvZu0aipj07Qfx+XIej7hhDrKGGoIxV5m5fT/kOJNJPQhQneRg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.2.0.tgz",
|
||||
"integrity": "sha512-LEJxRqfeomiiRd2t0uON6hxAtgOoWDfY3fugebbz+J3vDLO+SkdfFChQcOHTZhj9SYa9iwE9MGYNX72dKiOE4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hookified": "^1.11.0",
|
||||
"keyv": "^5.5.0"
|
||||
"@cacheable/memory": "^2.0.5",
|
||||
"@cacheable/utils": "^2.3.0",
|
||||
"hookified": "^1.13.0",
|
||||
"keyv": "^5.5.4",
|
||||
"qified": "^0.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable/node_modules/keyv": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.0.tgz",
|
||||
"integrity": "sha512-QG7qR2tijh1ftOvClut4YKKg1iW6cx3GZsKoGyJPxHkGWK9oJhG9P3j5deP0QQOGDowBMVQFaP+Vm4NpGYvmIQ==",
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz",
|
||||
"integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@keyv/serialize": "^1.1.0"
|
||||
"@keyv/serialize": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
|
|
@ -2339,6 +2403,20 @@
|
|||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -2678,6 +2756,19 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hashery": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.2.0.tgz",
|
||||
"integrity": "sha512-43XJKpwle72Ik5Zpam7MuzRWyNdwwdf6XHlh8wCj2PggvWf+v/Dm5B0dxGZOmddidgeO6Ofu9As/o231Ti/9PA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hookified": "^1.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
|
|
@ -2691,9 +2782,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/hookified": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.0.tgz",
|
||||
"integrity": "sha512-hMr1Y9TCLshScrBbV2QxJ9BROddxZ12MX9KsCtuGGy/3SmmN5H1PllKerrVlSotur9dlE8hmUKAOSa3WDzsZmQ==",
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.13.0.tgz",
|
||||
"integrity": "sha512-6sPYUY8olshgM/1LDNW4QZQN0IqgKhtl/1C8koNZBJrKLBk3AZl6chQtNwpNztvfiApHMEwMHek5rv993PRbWw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -3940,17 +4031,17 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picocrank": {
|
||||
"version": "1.8.7",
|
||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.8.7.tgz",
|
||||
"integrity": "sha512-A9eRkiGLtzCsi4aS+rkCw6MzPgEQUwDYNJJwGPGujwEtEKZcZk+wg9o/0yR/06qG3atip5H/aLGP7vPYS6iA5Q==",
|
||||
"version": "1.8.9",
|
||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.8.9.tgz",
|
||||
"integrity": "sha512-5NcLEYy4BSPhZm0tY8l/DLxaQoaOuHaf43S0MUgsKyLEiUIn9WnZGsDXHTlGnqNk6VK4+HnHs2rZnt3i5gj7FQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hugeicons/core-free-icons": "^1.2.1",
|
||||
"@hugeicons/core-free-icons": "^2.0.0",
|
||||
"@hugeicons/vue": "^1.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"femtocrank": "^2.4.11",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.2.2",
|
||||
"vite": "^7.2.4",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3"
|
||||
}
|
||||
|
|
@ -4189,6 +4280,19 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qified": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qified/-/qified-0.5.2.tgz",
|
||||
"integrity": "sha512-7gJ6mxcQb9vUBOtbKm5mDevbe2uRcOEVp1g4gb/Q+oLntB3HY8eBhOYRxFI2mlDFlY1e4DOSCptzxarXRvzxCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hookified": "^1.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
|
|
@ -4886,9 +4990,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/stylelint": {
|
||||
"version": "16.25.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz",
|
||||
"integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==",
|
||||
"version": "16.26.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.0.tgz",
|
||||
"integrity": "sha512-Y/3AVBefrkqqapVYH3LBF5TSDZ1kw+0XpdKN2KchfuhMK6lQ85S4XOG4lIZLcrcS4PWBmvcY6eS2kCQFz0jukQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -4915,7 +5019,7 @@
|
|||
"debug": "^4.4.3",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"file-entry-cache": "^10.1.4",
|
||||
"file-entry-cache": "^11.1.0",
|
||||
"global-modules": "^2.0.0",
|
||||
"globby": "^11.1.0",
|
||||
"globjoin": "^0.1.4",
|
||||
|
|
@ -5004,25 +5108,25 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/stylelint/node_modules/file-entry-cache": {
|
||||
"version": "10.1.4",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz",
|
||||
"integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.1.tgz",
|
||||
"integrity": "sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flat-cache": "^6.1.13"
|
||||
"flat-cache": "^6.1.19"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/flat-cache": {
|
||||
"version": "6.1.13",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.13.tgz",
|
||||
"integrity": "sha512-gmtS2PaUjSPa4zjObEIn4WWliKyZzYljgxODBfxugpK6q6HU9ClXzgCJ+nlcPKY9Bt090ypTOLIFWkV0jbKFjw==",
|
||||
"version": "6.1.19",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.19.tgz",
|
||||
"integrity": "sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cacheable": "^1.10.4",
|
||||
"cacheable": "^2.2.0",
|
||||
"flatted": "^3.3.3",
|
||||
"hookified": "^1.11.0"
|
||||
"hookified": "^1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/ignore": {
|
||||
|
|
@ -5441,9 +5545,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
|
@ -5565,13 +5669,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
|
||||
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.1.tgz",
|
||||
"integrity": "sha512-cc3Wx4eJZac9WMS8mxhfYiCipm9PBQ2Dz15piWYm7DwNcCehaKRgpolEdiqrjjT27T3Wijz3xJ7NeIc8ofIWAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.1.12",
|
||||
"@intlify/shared": "11.1.12",
|
||||
"@intlify/core-base": "11.2.1",
|
||||
"@intlify/shared": "11.2.1",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"source": "index.html",
|
||||
"devDependencies": {
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint": "^16.26.0",
|
||||
"stylelint-config-standard": "^39.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -22,19 +22,19 @@
|
|||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@connectrpc/connect": "^2.1.0",
|
||||
"@connectrpc/connect-web": "^2.1.0",
|
||||
"@hugeicons/core-free-icons": "^1.2.1",
|
||||
"@connectrpc/connect": "^2.1.1",
|
||||
"@connectrpc/connect-web": "^2.1.1",
|
||||
"@hugeicons/core-free-icons": "^2.0.0",
|
||||
"@hugeicons/vue": "^1.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.8.7",
|
||||
"picocrank": "^1.8.9",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.2.2",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vite": "^7.2.4",
|
||||
"vue-i18n": "^11.2.1",
|
||||
"vue-router": "^4.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// @generated by protoc-gen-es v2.10.0
|
||||
// @generated by protoc-gen-es v2.10.1
|
||||
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
|
|
@ -1421,15 +1421,15 @@ export declare type OAuth2Provider = Message<"olivetin.api.v1.OAuth2Provider"> &
|
|||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string url = 2;
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string icon = 3;
|
||||
*/
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string key = 4;
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,24 +9,24 @@
|
|||
<p>{{ initError }}</p>
|
||||
<p style="color: var(--fg2);">Please check your configuration and try again.</p>
|
||||
</section>
|
||||
<div v-else-if="dashboard">
|
||||
<template v-else-if="dashboard">
|
||||
<section v-if="dashboard.contents.length == 0">
|
||||
<legend>{{ dashboard.title }}</legend>
|
||||
<h2>{{ dashboard.title }}</h2>
|
||||
<p style = "text-align: center" class = "padding">This dashboard is empty.</p>
|
||||
</section>
|
||||
|
||||
<section class="transparent" v-else>
|
||||
<div v-for="component in dashboard.contents" :key="component.title">
|
||||
<fieldset>
|
||||
<legend v-if = "dashboard.title != 'Default'">{{ component.title }}</legend>
|
||||
<div class = "dashboard-row" v-for="component in dashboard.contents" :key="component.title">
|
||||
<h2 v-if = "dashboard.title != 'Default'">{{ component.title }}</h2>
|
||||
|
||||
<fieldset>
|
||||
<template v-for="subcomponent in component.contents">
|
||||
<DashboardComponent :component="subcomponent" />
|
||||
</template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
|
@ -154,7 +154,16 @@ onUnmounted(() => {
|
|||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
padding-top: 1.5em;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 180px);
|
||||
|
|
@ -163,6 +172,7 @@ fieldset {
|
|||
place-items: stretch;
|
||||
}
|
||||
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
:list="arg.suggestions ? `${arg.name}-choices` : undefined"
|
||||
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
|
||||
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
|
||||
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)" :required="arg.required"
|
||||
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
|
||||
@input="handleInput(arg, $event)" @change="handleChange(arg, $event)" />
|
||||
|
||||
<span class="argument-description" v-html="arg.description"></span>
|
||||
|
|
@ -160,6 +160,10 @@ function getInputType(arg) {
|
|||
return 'text'
|
||||
}
|
||||
|
||||
if (arg.type === 'datetime') {
|
||||
return 'datetime-local'
|
||||
}
|
||||
|
||||
return arg.type
|
||||
}
|
||||
|
||||
|
|
@ -198,6 +202,16 @@ async function validateArgument(arg, value) {
|
|||
return
|
||||
}
|
||||
|
||||
// Skip validation for datetime - backend will handle mangling values without seconds
|
||||
if (arg.type === 'datetime') {
|
||||
const inputElement = document.getElementById(arg.name)
|
||||
if (inputElement) {
|
||||
inputElement.setCustomValidity('')
|
||||
}
|
||||
delete formErrors.value[arg.name]
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const validateArgumentTypeArgs = {
|
||||
value: value,
|
||||
|
|
@ -282,10 +296,12 @@ async function startAction(actionArgs) {
|
|||
}
|
||||
|
||||
try {
|
||||
await window.client.startAction(startActionArgs)
|
||||
console.log('Action started successfully with tracking ID:', startActionArgs.uniqueTrackingId)
|
||||
const response = await window.client.startAction(startActionArgs)
|
||||
console.log('Action started successfully with tracking ID:', response.executionTrackingId)
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error('Failed to start action:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -315,9 +331,12 @@ async function handleSubmit(event) {
|
|||
const argvs = getArgumentValues()
|
||||
console.log('argument form has elements that passed validation')
|
||||
|
||||
await startAction(argvs)
|
||||
|
||||
router.back()
|
||||
try {
|
||||
const response = await startAction(argvs)
|
||||
router.push(`/logs/${response.executionTrackingId}`)
|
||||
} catch (err) {
|
||||
console.error('Failed to start action:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
</div>
|
||||
</Section>
|
||||
<template v-else>
|
||||
<Section v-for="def in entityDefinitions" :key="def.name" :title="'Entity: ' + def.title ">
|
||||
<Section v-for="def in entityDefinitions" :key="def.title" :title="'Entity: ' + def.title ">
|
||||
<div class = "section-content">
|
||||
<p>{{ def.instances.length }} instances.</p>
|
||||
|
||||
<ul>
|
||||
<li v-for="inst in def.instances" :key="inst.id">
|
||||
<li v-for="inst in def.instances" :key="inst.uniqueKey">
|
||||
<router-link :to="{ name: 'EntityDetails', params: { entityType: inst.type, entityKey: inst.uniqueKey } }">
|
||||
{{ inst.title }}
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
<div v-if="hasOAuth" class="login-oauth2">
|
||||
<h3>OAuth Login</h3>
|
||||
<div class="oauth-providers">
|
||||
<button v-for="provider in oauthProviders" :key="provider.name" class="oauth-button"
|
||||
<button v-for="provider in oauthProviders" :key="provider.key" class="oauth-button"
|
||||
@click="loginWithOAuth(provider)">
|
||||
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
|
||||
<span class="provider-name">Login with {{ provider.name }}</span>
|
||||
<span class="provider-name">Login with {{ provider.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -106,8 +106,13 @@ async function handleLocalLogin() {
|
|||
}
|
||||
|
||||
function loginWithOAuth(provider) {
|
||||
// Redirect to OAuth provider
|
||||
window.location.href = provider.authUrl
|
||||
if (!provider.key) {
|
||||
console.error('OAuth provider missing key:', provider)
|
||||
return
|
||||
}
|
||||
|
||||
const providerKey = encodeURIComponent(provider.key)
|
||||
window.location.href = `/oauth/login?provider=${providerKey}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -137,4 +142,12 @@ form {
|
|||
grid-template-columns: 1fr;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: .4em;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,14 +9,6 @@ aside {
|
|||
z-index: 3; /* Make sure the sidebar is on top of the terminal */
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 180px);
|
||||
grid-auto-rows: 1fr;
|
||||
justify-content: center;
|
||||
place-items: stretch;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-top: 4em;
|
||||
}
|
||||
|
|
@ -25,13 +17,6 @@ dialog {
|
|||
border-radius: 1em;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
---
|
||||
recursive: true
|
||||
require:
|
||||
- mochaSetup.mjs
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ test-install:
|
|||
|
||||
test-run:
|
||||
# GitHub Actions fails badly on the default timeout of 2000ms
|
||||
npx mocha -t 10000
|
||||
npx mocha tests --recursive -t 10000
|
||||
|
||||
find-flakey-tests:
|
||||
echo "Running test-run infinately"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.provision "shell", inline: "mkdir /etc/OliveTin && chmod o+w /etc/OliveTin/ && mkdir -p /opt/OliveTin-configs/ && chmod 0777 /opt/OliveTin-configs", privileged: true
|
||||
config.vm.provision "file", source: "configs/.", destination: "/opt/OliveTin-configs/"
|
||||
config.vm.provision "file", source: "tests/.", destination: "/tmp/test-configs/"
|
||||
config.vm.provision "shell", inline: "for dir in /tmp/test-configs/*/; do if [ -f \"$dir/config.yaml\" ]; then cp -r \"$dir\" /opt/OliveTin-configs/$(basename \"$dir\")/; fi; done", privileged: true
|
||||
|
||||
config.vm.provider :libvirt do |libvirt|
|
||||
libvirt.management_network_device = 'virbr0'
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
pageTitle: "My Custom App"
|
||||
|
||||
actions:
|
||||
- title: sleep 2 seconds
|
||||
shell: sleep 2
|
||||
icon: "🥱"
|
||||
|
|
@ -9,13 +9,13 @@
|
|||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"wait-on": "^9.0.1"
|
||||
"wait-on": "^9.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^6.2.0",
|
||||
"eslint": "^9.37.0",
|
||||
"mocha": "^11.7.4",
|
||||
"selenium-webdriver": "^4.36.0"
|
||||
"chai": "^6.2.1",
|
||||
"eslint": "^9.39.1",
|
||||
"mocha": "^11.7.5",
|
||||
"selenium-webdriver": "^4.38.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
|
@ -77,13 +77,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
|
|
@ -92,22 +92,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
|
||||
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0"
|
||||
"@eslint/core": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
|
||||
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
|
@ -142,9 +142,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
|
||||
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -155,9 +155,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
|
|
@ -165,13 +165,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
|
||||
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -421,9 +421,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
|
|
@ -490,9 +490,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz",
|
||||
"integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz",
|
||||
"integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -818,25 +818,24 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.4.0",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.37.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
|
|
@ -1087,9 +1086,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
|
|
@ -1642,9 +1641,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "11.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.4.tgz",
|
||||
"integrity": "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w==",
|
||||
"version": "11.7.5",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz",
|
||||
"integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1953,9 +1952,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/selenium-webdriver": {
|
||||
"version": "4.36.0",
|
||||
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.36.0.tgz",
|
||||
"integrity": "sha512-rZGqjXiqNVL6QNqKNEk5DPaIMPbvApcmAS9QsXyt5wT3sfTSHGCh4AX/YKeDTOwei1BOZDlPOKBd82WCosUt9w==",
|
||||
"version": "4.38.0",
|
||||
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.38.0.tgz",
|
||||
"integrity": "sha512-5/UXXFSQmn7FGQkbcpAqvfhzflUdMWtT7QqpEgkFD6Q6rDucxB5EUfzgjmr6JbUj30QodcW3mDXehzoeS/Vy5w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2210,12 +2209,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.1.tgz",
|
||||
"integrity": "sha512-noeCAI+XbqWMXY23sKril0BSURhuLYarkVXwJv1uUWwoojZJE7pmX3vJ7kh7SZaNgPGzfsCSQIZM/AGvu0Q9pA==",
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz",
|
||||
"integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.2",
|
||||
"joi": "^18.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.8",
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@
|
|||
"author": "",
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"chai": "^6.2.0",
|
||||
"eslint": "^9.37.0",
|
||||
"mocha": "^11.7.4",
|
||||
"selenium-webdriver": "^4.36.0"
|
||||
"chai": "^6.2.1",
|
||||
"eslint": "^9.39.1",
|
||||
"mocha": "^11.7.5",
|
||||
"selenium-webdriver": "^4.38.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"wait-on": "^9.0.1"
|
||||
"wait-on": "^9.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
|
|||
|
||||
console.log(" OliveTin starting local process...")
|
||||
|
||||
this.ot = spawn('./../service/OliveTin', ['-configdir', 'configs/' + cfg + '/'])
|
||||
this.ot = spawn('./../service/OliveTin', ['-configdir', 'tests/' + cfg + '/'])
|
||||
|
||||
let logStdout = false
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { By, until } from 'selenium-webdriver'
|
|||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: authRequireGuestsToLogin', function () {
|
||||
this.timeout(30000)
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
openSidebar,
|
||||
getNavigationLinks,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: dashboards with basic fieldsets', function () {
|
||||
before(async function () {
|
||||
|
|
@ -42,15 +42,15 @@ describe('config: dashboards with basic fieldsets', function () {
|
|||
expect(actionButtons).to.have.length(5, 'Expected 5 action buttons')
|
||||
|
||||
// Check that we have the expected number of fieldsets
|
||||
const allFieldsets = await webdriver.findElements(By.css('fieldset'))
|
||||
expect(allFieldsets).to.have.length(3, 'Expected 3 fieldsets total')
|
||||
const dashboardRows = await webdriver.findElements(By.css('.dashboard-row'))
|
||||
expect(dashboardRows).to.have.length(3, 'Expected 3 dashboard rows total')
|
||||
|
||||
// Check that we have fieldsets with the expected titles
|
||||
const fieldsetTitles = []
|
||||
for (let i = 0; i < allFieldsets.length; i++) {
|
||||
const legend = await allFieldsets[i].findElements(By.css('legend'))
|
||||
if (legend.length > 0) {
|
||||
const title = await legend[0].getText()
|
||||
for (let i = 0; i < dashboardRows.length; i++) {
|
||||
const titleElements = await dashboardRows[i].findElements(By.css('h2'))
|
||||
if (titleElements.length > 0) {
|
||||
const title = await titleElements[0].getText()
|
||||
fieldsetTitles.push(title)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: Test datetime argument
|
||||
shell: "echo 'Selected datetime: {{ datetime }}'"
|
||||
icon: ping
|
||||
arguments:
|
||||
- name: datetime
|
||||
title: Select a date and time
|
||||
type: datetime
|
||||
description: Choose a date and time for the action
|
||||
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButton,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: datetime', function () {
|
||||
before(async function () {
|
||||
await runner.start('datetime')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||
})
|
||||
|
||||
it('Datetime argument uses datetime-local input type', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const btn = await getActionButton(webdriver, 'Test datetime argument')
|
||||
|
||||
await btn.click()
|
||||
|
||||
// Wait for navigation to argument form page
|
||||
await webdriver.wait(
|
||||
new Condition('wait for argument form page', async () => {
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
return url.includes('/actionBinding/') && url.includes('/argumentForm')
|
||||
}),
|
||||
8000
|
||||
)
|
||||
|
||||
// Find the datetime input field
|
||||
const datetimeInput = await webdriver.findElement(By.id('datetime'))
|
||||
|
||||
// Verify it's a datetime-local input type
|
||||
const inputType = await datetimeInput.getAttribute('type')
|
||||
expect(inputType).to.equal('datetime-local', 'Input type should be datetime-local')
|
||||
|
||||
// Verify it has the step attribute set to 1 (for seconds precision)
|
||||
const step = await datetimeInput.getAttribute('step')
|
||||
expect(step).to.equal('1', 'Step attribute should be 1')
|
||||
|
||||
// Verify the label is present
|
||||
const label = await webdriver.findElement(By.css('label[for="datetime"]'))
|
||||
expect(await label.getText()).to.contain('Select a date and time')
|
||||
})
|
||||
|
||||
it('Datetime argument can be filled and submitted', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const btn = await getActionButton(webdriver, 'Test datetime argument')
|
||||
|
||||
await btn.click()
|
||||
|
||||
// Wait for navigation to argument form page
|
||||
await webdriver.wait(
|
||||
new Condition('wait for argument form page', async () => {
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
return url.includes('/actionBinding/') && url.includes('/argumentForm')
|
||||
}),
|
||||
8000
|
||||
)
|
||||
|
||||
// Find the datetime input field
|
||||
const datetimeInput = await webdriver.findElement(By.id('datetime'))
|
||||
|
||||
// Set a datetime value (format: YYYY-MM-DDTHH:mm)
|
||||
// datetime-local returns values without seconds, backend will add :00
|
||||
const testDateTime = '2023-12-25T15:30'
|
||||
|
||||
// Use JavaScript to set the value directly (more reliable for datetime-local inputs)
|
||||
await webdriver.executeScript(
|
||||
'arguments[0].value = arguments[1]',
|
||||
datetimeInput,
|
||||
testDateTime
|
||||
)
|
||||
|
||||
// Trigger input event to ensure Vue reactivity
|
||||
await webdriver.executeScript(
|
||||
'arguments[0].dispatchEvent(new Event("input", { bubbles: true }))',
|
||||
datetimeInput
|
||||
)
|
||||
|
||||
// Small wait for Vue to process the change
|
||||
await webdriver.sleep(100)
|
||||
|
||||
// Verify the value was set
|
||||
const value = await datetimeInput.getAttribute('value')
|
||||
expect(value).to.equal(testDateTime)
|
||||
|
||||
// Find and click the submit button
|
||||
const submitButton = await webdriver.findElement(
|
||||
By.css('button[name="start"]')
|
||||
)
|
||||
await submitButton.click()
|
||||
|
||||
// Wait for navigation to logs page
|
||||
await webdriver.wait(
|
||||
new Condition('wait for logs page', async () => {
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
return url.includes('/logs/')
|
||||
}),
|
||||
8000
|
||||
)
|
||||
|
||||
// Verify we're on the logs page (action was executed)
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
expect(url).to.include('/logs/')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ import {
|
|||
openSidebar,
|
||||
getNavigationLinks,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: empty dashboards are hidden', function () {
|
||||
before(async function () {
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
getRootAndWait,
|
||||
takeScreenshot,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: entities', function () {
|
||||
before(async function () {
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: entities', function () {
|
||||
before(async function () {
|
||||
|
|
@ -7,7 +7,7 @@ import {
|
|||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
openSidebar,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: general', function () {
|
||||
before(async function () {
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: hiddenFooter', function () {
|
||||
before(async function () {
|
||||
|
|
@ -4,7 +4,7 @@ import {
|
|||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
|
||||
describe('config: hiddenNav', function () {
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: include', function () {
|
||||
this.timeout(30000)
|
||||
|
|
@ -4,7 +4,7 @@ import { By, until, Condition } from 'selenium-webdriver'
|
|||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: localAuth', function () {
|
||||
this.timeout(30000) // Increase timeout to 30 seconds
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
|
||||
describe('config: multipleDropdowns', function () {
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
#
|
||||
# Integration Test Config: GitHub OAuth2 Authentication
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
# Enable GitHub OAuth2 authentication
|
||||
authOAuth2RedirectUrl: "http://localhost:1337/oauth2/callback"
|
||||
authOAuth2Providers:
|
||||
github:
|
||||
name: github
|
||||
title: "Good old GitHub"
|
||||
clientId: "test-client-id"
|
||||
clientSecret: "test-client-secret"
|
||||
|
||||
# Require login for guests
|
||||
authRequireGuestsToLogin: true
|
||||
|
||||
# Simple actions for testing
|
||||
actions:
|
||||
- title: Ping Google.com
|
||||
shell: echo "ping google.com"
|
||||
icon: ping
|
||||
|
||||
- title: sleep 2 seconds
|
||||
shell: sleep 2
|
||||
icon: "🥱"
|
||||
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: githubOAuth', function () {
|
||||
this.timeout(30000)
|
||||
|
||||
before(async function () {
|
||||
await runner.start('oauthLoginGithub')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||
})
|
||||
|
||||
it('Server starts successfully with GitHub OAuth enabled', async function () {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
// Wait for the page to load
|
||||
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||
|
||||
// Check that the page loaded
|
||||
const title = await webdriver.getTitle()
|
||||
expect(title).to.contain('OliveTin')
|
||||
|
||||
console.log('Server started successfully with GitHub OAuth enabled')
|
||||
})
|
||||
|
||||
it('Login page is accessible and shows GitHub OAuth button', async function () {
|
||||
// Navigate to login page
|
||||
await webdriver.get(runner.baseUrl() + '/login')
|
||||
|
||||
// Wait for the page to load
|
||||
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||
|
||||
// Wait for Vue to render
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// Check if OAuth section is present
|
||||
const oauthSection = await webdriver.findElements(By.css('.login-oauth2'))
|
||||
expect(oauthSection.length).to.be.greaterThan(0, 'OAuth login section should be present')
|
||||
|
||||
// Check for GitHub OAuth button
|
||||
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||
|
||||
// Find the GitHub button specifically
|
||||
// Button may show "Login with GitHub" or "Login with undefined" depending on provider.name vs provider.title
|
||||
// We'll check for the presence of the button and verify it's in the OAuth section
|
||||
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||
|
||||
// The first button should be GitHub since it's the only provider in the config
|
||||
const githubButton = githubButtons[0]
|
||||
const buttonText = await githubButton.getText()
|
||||
|
||||
// Button should contain "Login with" and the provider should be configured as GitHub
|
||||
expect(buttonText).to.include('Login with', 'Button should have "Login with" prefix')
|
||||
|
||||
console.log('GitHub OAuth button found with text:', buttonText)
|
||||
})
|
||||
|
||||
it('GitHub OAuth button has correct structure and is clickable', async function () {
|
||||
await webdriver.get(runner.baseUrl() + '/login')
|
||||
|
||||
// Wait for the page to load
|
||||
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// Find GitHub OAuth button
|
||||
// Since the test config only has one provider (GitHub), we can use the first button
|
||||
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||
|
||||
const githubButton = githubButtons[0]
|
||||
const buttonText = await githubButton.getText()
|
||||
console.log('Button text:', buttonText)
|
||||
|
||||
// Verify it's the GitHub button (should contain "github" in the text)
|
||||
expect(buttonText.toLowerCase()).to.include('github', 'Button should be GitHub OAuth button')
|
||||
|
||||
// Check for provider icon (if present)
|
||||
const providerIcons = await githubButton.findElements(By.css('.provider-icon'))
|
||||
const providerNames = await githubButton.findElements(By.css('.provider-name'))
|
||||
// Provider name may show "GitHub" (from title) or be undefined (if using name field)
|
||||
// Just verify the structure is present
|
||||
if (providerNames.length > 0) {
|
||||
const providerNameText = await providerNames[0].getText()
|
||||
expect(providerNameText.toLowerCase()).to.include('github', 'Provider name should include "github"')
|
||||
expect(providerNameText).to.include('Login with', 'Provider name should have "Login with" prefix')
|
||||
console.log('Provider name text:', providerNameText)
|
||||
}
|
||||
|
||||
console.log('GitHub OAuth button structure verified')
|
||||
})
|
||||
|
||||
it('Clicking GitHub OAuth button redirects to GitHub OAuth URL', async function () {
|
||||
await webdriver.get(runner.baseUrl() + '/login')
|
||||
|
||||
// Wait for the page to load
|
||||
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// Find GitHub OAuth button (should be the first/only one in our test config)
|
||||
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||
expect(githubButtons.length).to.be.greaterThan(0, 'OAuth button should be present')
|
||||
|
||||
const githubButton = githubButtons[0]
|
||||
|
||||
// Get the current URL before clicking
|
||||
const initialUrl = await webdriver.getCurrentUrl()
|
||||
|
||||
// Click the button
|
||||
await githubButton.click()
|
||||
|
||||
// Wait for navigation (OAuth redirect happens via window.location.href)
|
||||
// Since we can't actually complete OAuth flow, we check that the button
|
||||
// click handler is set up correctly by verifying the button exists and is clickable
|
||||
// In a real scenario, this would redirect to GitHub's OAuth page
|
||||
|
||||
// Give a small delay to allow any navigation to start
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Note: We can't fully test the OAuth redirect in integration tests without
|
||||
// a real GitHub OAuth app, but we've verified the button exists and is functional
|
||||
console.log('GitHub OAuth button click verified (redirect would happen in production)')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
openSidebar,
|
||||
closeSidebar,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: onlyDashboards', function () {
|
||||
before(async function () {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
import { By } from 'selenium-webdriver'
|
||||
import { expect } from 'chai'
|
||||
|
|
@ -4,7 +4,7 @@ import { expect } from 'chai'
|
|||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
let metrics = [
|
||||
{'name': 'olivetin_actions_requested_count', 'type': 'counter', 'desc': 'The actions requested count'},
|
||||
|
|
@ -9,7 +9,7 @@ import {
|
|||
requireExecutionDialogStatus,
|
||||
getRootAndWait,
|
||||
getActionButton
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: sleep', function () {
|
||||
before(async function () {
|
||||
|
|
@ -2,7 +2,7 @@ import { expect } from 'chai'
|
|||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: trustedHeader', function () {
|
||||
before(async function () {
|
||||
|
|
@ -8,4 +8,4 @@ require (
|
|||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
require golang.org/x/sys v0.38.0 // indirect
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -329,8 +329,8 @@ message AdditionalLink {
|
|||
|
||||
message OAuth2Provider {
|
||||
string title = 1;
|
||||
string url = 2;
|
||||
string icon = 3;
|
||||
string key = 4;
|
||||
}
|
||||
|
||||
message GetActionBindingRequest {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/api"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func printPwd() {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting working directory: %v", err)
|
||||
}
|
||||
log.Infof("Working directory: %s", pwd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
resetPasswords := flag.Bool("passwords", true, "Reset passwords")
|
||||
flag.Parse()
|
||||
|
||||
log.Info("Config tool started")
|
||||
|
||||
printPwd()
|
||||
|
||||
k := koanf.New(".")
|
||||
|
||||
configPath, err := filepath.Abs("../config.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting absolute config path: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Loading config from %s", configPath)
|
||||
|
||||
backupOriginalConfig(configPath)
|
||||
|
||||
err = k.Load(file.Provider(configPath), yaml.Parser())
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading config: %v", err)
|
||||
}
|
||||
|
||||
cfg := &config.Config{}
|
||||
|
||||
config.AppendSource(cfg, k, configPath)
|
||||
|
||||
if *resetPasswords {
|
||||
resetAllPasswords(k, cfg)
|
||||
}
|
||||
|
||||
saveConfig(configPath, k)
|
||||
}
|
||||
|
||||
func backupOriginalConfig(configPath string) {
|
||||
originalConfigPath := filepath.Join(filepath.Dir(configPath), "config.original.yaml")
|
||||
|
||||
_, err := os.Stat(originalConfigPath)
|
||||
if err == nil {
|
||||
log.Infof("Backup already exists at %s, skipping backup to preserve original", originalConfigPath)
|
||||
return
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
log.Fatalf("Error checking backup file: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading config for backup: %v", err)
|
||||
}
|
||||
err = os.WriteFile(originalConfigPath, data, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Error writing backup config: %v", err)
|
||||
}
|
||||
log.Infof("Original config backed up to %s", originalConfigPath)
|
||||
}
|
||||
|
||||
func resetAllPasswords(k *koanf.Koanf, cfg *config.Config) {
|
||||
if !cfg.AuthLocalUsers.Enabled || len(cfg.AuthLocalUsers.Users) == 0 {
|
||||
log.Info("No local users found, skipping password reset")
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := api.CreateHash("password")
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating password hash: %v", err)
|
||||
}
|
||||
|
||||
usersSlice := k.Get("authLocalUsers.users")
|
||||
usersSliceTyped, ok := usersSlice.([]interface{})
|
||||
|
||||
if ok && len(usersSliceTyped) > 0 {
|
||||
newUsersSlice := make([]interface{}, len(usersSliceTyped))
|
||||
for index, userValue := range usersSliceTyped {
|
||||
userMap, ok := userValue.(map[string]interface{})
|
||||
if !ok {
|
||||
log.Warnf("User entry at index %d is not a map, skipping", index)
|
||||
newUsersSlice[index] = userValue
|
||||
continue
|
||||
}
|
||||
|
||||
oldPassword, _ := userMap["password"].(string)
|
||||
username, _ := userMap["username"].(string)
|
||||
if username == "" {
|
||||
username = fmt.Sprintf("user[%d]", index)
|
||||
}
|
||||
|
||||
newUserMap := make(map[string]interface{})
|
||||
for k, v := range userMap {
|
||||
newUserMap[k] = v
|
||||
}
|
||||
newUserMap["password"] = hashedPassword
|
||||
newUsersSlice[index] = newUserMap
|
||||
|
||||
oldHashPreview := oldPassword
|
||||
if len(oldPassword) > 20 {
|
||||
oldHashPreview = oldPassword[:20]
|
||||
}
|
||||
log.Infof("Reset password for user '%s' (old hash: %s...)", username, oldHashPreview)
|
||||
}
|
||||
err = k.Set("authLocalUsers.users", newUsersSlice)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Fatalf("Error setting users")
|
||||
}
|
||||
} else {
|
||||
for index, user := range cfg.AuthLocalUsers.Users {
|
||||
key := "authLocalUsers.users." + strconv.Itoa(index) + ".password"
|
||||
err = k.Set(key, hashedPassword)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Fatalf("Error setting user password")
|
||||
}
|
||||
|
||||
oldHashPreview := user.Password
|
||||
if len(oldHashPreview) > 20 {
|
||||
oldHashPreview = oldHashPreview[:20]
|
||||
}
|
||||
log.Infof("Reset password for user '%s' (old hash: %s...)", user.Username, oldHashPreview)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Reset %d password(s) to 'password'", len(cfg.AuthLocalUsers.Users))
|
||||
}
|
||||
|
||||
func saveConfig(configPath string, k *koanf.Koanf) {
|
||||
out, err := k.Marshal(yaml.Parser())
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error marshalling config: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(configPath, out, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Error saving config: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Config saved to %s", configPath)
|
||||
}
|
||||
|
|
@ -3390,8 +3390,8 @@ func (x *AdditionalLink) GetUrl() string {
|
|||
type OAuth2Provider struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||
Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -3433,16 +3433,16 @@ func (x *OAuth2Provider) GetTitle() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (x *OAuth2Provider) GetUrl() string {
|
||||
func (x *OAuth2Provider) GetIcon() string {
|
||||
if x != nil {
|
||||
return x.Url
|
||||
return x.Icon
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *OAuth2Provider) GetIcon() string {
|
||||
func (x *OAuth2Provider) GetKey() string {
|
||||
if x != nil {
|
||||
return x.Icon
|
||||
return x.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -4003,9 +4003,9 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
|||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
||||
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
||||
"\x0eOAuth2Provider\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
||||
"\x03url\x18\x02 \x01(\tR\x03url\x12\x12\n" +
|
||||
"\x04icon\x18\x03 \x01(\tR\x04icon\"8\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" +
|
||||
"\x04icon\x18\x03 \x01(\tR\x04icon\x12\x10\n" +
|
||||
"\x03key\x18\x04 \x01(\tR\x03key\"8\n" +
|
||||
"\x17GetActionBindingRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"binding_id\x18\x01 \x01(\tR\tbindingId\"K\n" +
|
||||
|
|
|
|||
128
service/go.mod
128
service/go.mod
|
|
@ -7,72 +7,75 @@ toolchain go1.24.9
|
|||
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.18.1
|
||||
connectrpc.com/connect v1.19.1
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/MicahParks/keyfunc/v3 v3.4.0
|
||||
github.com/MicahParks/keyfunc/v3 v3.7.0
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/bufbuild/buf v1.55.1
|
||||
github.com/bufbuild/buf v1.60.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/fzipp/gocyclo v0.6.0
|
||||
github.com/go-critic/go-critic v0.13.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/go-critic/go-critic v0.14.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4
|
||||
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.0
|
||||
github.com/knadh/koanf/providers/env v1.1.0
|
||||
github.com/knadh/koanf/providers/file v1.2.0
|
||||
github.com/knadh/koanf/providers/rawbytes v1.0.0
|
||||
github.com/knadh/koanf/v2 v2.3.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sys v0.35.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.akshayshah.org/connectproto v0.6.0
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
golang.org/x/sys v0.38.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250616221922-7d6913ad2095.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250616221922-7d6913ad2095.1 // indirect
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 // indirect
|
||||
buf.build/go/app v0.1.0 // indirect
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.10-20250718181942-e35f9b667443.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.10-20250109164928-1da0de137947.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251027152159-f1066ce064ca.2 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.10-20251027152159-f1066ce064ca.1 // indirect
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.10-20241007202033-cf42259fcbfc.1 // indirect
|
||||
buf.build/go/app v0.2.0 // indirect
|
||||
buf.build/go/bufplugin v0.9.0 // indirect
|
||||
buf.build/go/bufprivateusage v0.1.0 // indirect
|
||||
buf.build/go/interrupt v1.1.0 // indirect
|
||||
buf.build/go/protovalidate v0.13.1 // indirect
|
||||
buf.build/go/protovalidate v1.0.1 // indirect
|
||||
buf.build/go/protoyaml v0.6.0 // indirect
|
||||
buf.build/go/spdx v0.2.0 // indirect
|
||||
buf.build/go/standard v0.1.0 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
connectrpc.com/otelconnect v0.7.2 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
connectrpc.com/otelconnect v0.8.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/MicahParks/jwkset v0.9.6 // indirect
|
||||
github.com/MicahParks/jwkset v0.11.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.14.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.14.2-0.20251120233202-3f9009bcd6c8 // indirect
|
||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cristalhq/acmd v0.12.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v28.3.1+incompatible // indirect
|
||||
github.com/docker/cli v29.0.2+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.4 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.2 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
|
|
@ -84,15 +87,15 @@ require (
|
|||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/cel-go v0.25.0 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.20.6 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jdx/go-netrc v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
|
|
@ -106,52 +109,55 @@ require (
|
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.4 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/encoding v0.5.1 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/spf13/cobra v1.10.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
go.akshayshah.org/connectproto v0.6.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.10.1 // indirect
|
||||
github.com/tidwall/btree v1.8.1 // indirect
|
||||
github.com/vbatts/tar-split v0.12.2 // indirect
|
||||
go.lsp.dev/jsonrpc2 v0.10.0 // indirect
|
||||
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
|
||||
go.lsp.dev/protocol v0.12.0 // indirect
|
||||
go.lsp.dev/uri v0.3.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
pluginrpc.com/pluginrpc v0.5.0 // indirect
|
||||
)
|
||||
|
|
|
|||
139
service/go.sum
139
service/go.sum
|
|
@ -1,21 +1,39 @@
|
|||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 h1:f6miF8tK6H+Ktad24WpnNfpHO75GRGk0rhJ1mxPXqgA=
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1/go.mod h1:rvbyamNtvJ4o3ExeCmaG5/6iHnu0vy0E+UQ+Ph0om8s=
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.10-20250718181942-e35f9b667443.1 h1:FzJGrb8r7vir+P3zJ5Ebey8p54LYTYtQsrM/U35YO9Q=
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.10-20250718181942-e35f9b667443.1/go.mod h1:E6HwqUm4Ag7bXtg/tX7jHWO7CgpknbmeACgDax0icV0=
|
||||
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.10-20250109164928-1da0de137947.1 h1:9hkMnVoImDlY7rTlAWIWXdkGUKOjf3YlyZeSbYT29uA=
|
||||
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.10-20250109164928-1da0de137947.1/go.mod h1:/AouMCAeQ+kB7+RRFpdUlZe3503p18VoUNcU2AFqZXM=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 h1:6tCo3lsKNLqUjRPhyc8JuYWYUiQkulufxSDOfG1zgWQ=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1 h1:31on4W/yPcV4nZHL4+UCiCvLPsMqe/vJcNg8Rci0scc=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1/go.mod h1:fUl8CEN/6ZAMk6bP8ahBJPUJw7rbp+j4x+wCcYi2IG4=
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250616221922-7d6913ad2095.1 h1:YNqHDUUykdS+vw3oHKiNj8tc+63zzZEEiOdleUuD3M4=
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250616221922-7d6913ad2095.1/go.mod h1:t6+CtfVRycblgZmLx9b4YUu3C4qnt+arMgcUDXBXriI=
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251027152159-f1066ce064ca.2 h1:Dbh4Edwy5qHlz1/boPAQ7T5Q7ZDMgEuQlEbXa94+JEo=
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251027152159-f1066ce064ca.2/go.mod h1:SqqTA3aiYVDkpDINxgbxDT6QBjkVjdqUXtbiz6DiWIg=
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250616221922-7d6913ad2095.1 h1:ZcKucfxX7jiZcQ9Gudh22+hgZoQOLaSyl12SLX/C97c=
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250616221922-7d6913ad2095.1/go.mod h1:bUPpZtzAkcnTA7OLfKCvkvkxEAC6dG/ZIlbnbUJicL4=
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.10-20251027152159-f1066ce064ca.1 h1:5tUFlRgcC+N2JJtjwlwyb2J4bBk/bJYLXk50zlewtzk=
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.10-20251027152159-f1066ce064ca.1/go.mod h1:AaYXXeRvnOc151wEuupAmn58Mh9bccKce2kk3QKMIrQ=
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 h1:trcsXBDm8exui7mvndZnvworCyBq1xuMnod2N0j79K8=
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1/go.mod h1:OUbhXurY+VHFGn9FBxcRy8UB7HXk9NvJ2qCgifOMypQ=
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.10-20241007202033-cf42259fcbfc.1 h1:CzM0kZcoaIr8+R4i8QVorUNRM/CqMr87i3j+w2pdpCc=
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.10-20241007202033-cf42259fcbfc.1/go.mod h1:bG+Fa7tcA+4pW0JdOh4h7iKjleyZIKhfVzVS10qfrnk=
|
||||
buf.build/go/app v0.1.0 h1:nlqD/h0rhIN73ZoiDElprrPiO2N6JV+RmNK34K29Ihg=
|
||||
buf.build/go/app v0.1.0/go.mod h1:0XVOYemubVbxNXVY0DnsVgWeGkcbbAvjDa1fmhBC+Wo=
|
||||
buf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8=
|
||||
buf.build/go/app v0.2.0/go.mod h1:0XVOYemubVbxNXVY0DnsVgWeGkcbbAvjDa1fmhBC+Wo=
|
||||
buf.build/go/bufplugin v0.9.0 h1:ktZJNP3If7ldcWVqh46XKeiYJVPxHQxCfjzVQDzZ/lo=
|
||||
buf.build/go/bufplugin v0.9.0/go.mod h1:Z0CxA3sKQ6EPz/Os4kJJneeRO6CjPeidtP1ABh5jPPY=
|
||||
buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4=
|
||||
buf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w=
|
||||
buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE=
|
||||
buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=
|
||||
buf.build/go/protovalidate v0.13.1 h1:6loHDTWdY/1qmqmt1MijBIKeN4T9Eajrqb9isT1W1s8=
|
||||
buf.build/go/protovalidate v0.13.1/go.mod h1:C/QcOn/CjXRn5udUwYBiLs8y1TGy7RS+GOSKqjS77aU=
|
||||
buf.build/go/protovalidate v1.0.1 h1:Fwmf08OOUuKVeMvEnDmcKxQam4PJc/zFgvVX64BhTms=
|
||||
buf.build/go/protovalidate v1.0.1/go.mod h1:SoZmvk/3ZzOVg9YSkTdm4grMAByjf8zgZq4ZNaLZXoQ=
|
||||
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
||||
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
|
||||
buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
|
||||
|
|
@ -24,18 +42,28 @@ buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U=
|
|||
buf.build/go/standard v0.1.0/go.mod h1:PiqpHz/7ZFq+kqvYhc/SK3lxFIB9N/aiH2CFC2JHIQg=
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
|
||||
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
|
||||
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
|
||||
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||
connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI=
|
||||
connectrpc.com/otelconnect v0.7.2/go.mod h1:JS7XUKfuJs2adhCnXhNHPHLz6oAaZniCJdSF00OZSew=
|
||||
connectrpc.com/otelconnect v0.8.0 h1:a4qrN4H8aEE2jAoCxheZYYfEjXMgVPyL9OzPQLBEFXU=
|
||||
connectrpc.com/otelconnect v0.8.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/MicahParks/jwkset v0.9.6 h1:Tf8l2/MOby5Kh3IkrqzThPQKfLytMERoAsGZKlyYZxg=
|
||||
github.com/MicahParks/jwkset v0.9.6/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
|
||||
github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
|
||||
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
|
||||
github.com/MicahParks/keyfunc/v3 v3.4.0 h1:g03TXq6NjhZyO/UkODl//abm4KiLLNRi0VhW7vGOHyg=
|
||||
github.com/MicahParks/keyfunc/v3 v3.4.0/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
|
||||
github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
|
||||
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
|
|
@ -46,8 +74,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bufbuild/buf v1.55.1 h1:yaRXO9YmtgyEhiqT/gwuJWhHN9xBBbqlQvXVnPauvCk=
|
||||
github.com/bufbuild/buf v1.55.1/go.mod h1:bvDF6WkvObC+ca9gmP++/oCAWeVVX7MspMcTFznqF7k=
|
||||
github.com/bufbuild/buf v1.60.0 h1:hJM7Ub6wVvQ1IeEl+jVu8LUJgo9BTSplXAUENi8tkw8=
|
||||
github.com/bufbuild/buf v1.60.0/go.mod h1:R1377eTyYbYjuak5nFUn1TSlG3ipgHHT6xQTupkWtP4=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/bufbuild/protocompile v0.14.2-0.20251120233202-3f9009bcd6c8 h1:l4PKzJ7Usff8j5/e+YaWZPaM+rJHIghgDxRn8vDNxNo=
|
||||
github.com/bufbuild/protocompile v0.14.2-0.20251120233202-3f9009bcd6c8/go.mod h1:HKN246DRQwavs64sr2xYmSL+RFOFxmLti+WGCZ2jh9U=
|
||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
|
||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
|
|
@ -62,6 +94,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
|||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
|
|
@ -76,14 +110,22 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v28.3.1+incompatible h1:ZUdwOLDEBoE3TE5rdC9IXGY5HPHksJK3M+hJEWhh2mc=
|
||||
github.com/docker/cli v28.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.0.2+incompatible h1:iLuKy2GWOSLXGp8feLYBJQVDv7m/8xoofz6lPq41x6A=
|
||||
github.com/docker/cli v29.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
|
||||
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
|
|
@ -94,8 +136,12 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
|||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY=
|
||||
github.com/go-critic/go-critic v0.13.0/go.mod h1:M/YeuJ3vOCQDnP2SU+ZhjgRzwzcBW87JqLpMJLrZDLI=
|
||||
github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo=
|
||||
github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
|
@ -124,12 +170,18 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
|||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
|
||||
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
|
||||
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
||||
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
|
|
@ -146,6 +198,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4 h1:MIZEqAaeMP1/saH0w6I5mzGKSv2lw8fAO7Hm2FgJb9k=
|
||||
github.com/jamesread/golure v0.0.0-20250619190948-fa38cbd93cc4/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
||||
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c h1:v8gN2xXFQjkF0PsoGSqDviRNmPHcBsvl6rMSbvXz1sM=
|
||||
github.com/jamesread/golure v0.0.0-20250919212919-976d085a100c/go.mod h1:BZ/CMtZJJ4LNEBDSjGfafTJMjlDPIA9FS16+reN9NUE=
|
||||
github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
|
||||
github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
|
||||
|
|
@ -154,6 +208,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
|||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
|
|
@ -200,6 +256,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
|
||||
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
|
@ -208,14 +266,22 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/quasilyte/go-ruleguard v0.4.4 h1:53DncefIeLX3qEpjzlS1lyUmQoUEeOWPFWqaTJq9eAQ=
|
||||
github.com/quasilyte/go-ruleguard v0.4.4/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
|
||||
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
|
||||
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
|
||||
|
|
@ -224,26 +290,43 @@ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4l
|
|||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
|
||||
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/segmentio/encoding v0.5.1 h1:LhmgXA5/alniiqfc4cYYrxF6DbUQ3m8MVz4/LQIU1mg=
|
||||
github.com/segmentio/encoding v0.5.1/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
|
@ -255,10 +338,18 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
|
||||
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
|
||||
github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
|
||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
|
||||
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
|
|
@ -274,32 +365,52 @@ go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
|
|||
go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
|
@ -309,18 +420,26 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251113190631-e25ba8c21ef6 h1:8dPTIY8FDvi6k5oSD/GuDbs0QyC+A53U8psHrD7K3jw=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -331,8 +450,12 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -340,6 +463,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -356,6 +481,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
@ -363,6 +490,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
|
@ -371,8 +500,12 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
|
|
@ -381,14 +514,20 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
|
|
@ -26,57 +21,7 @@ func (p PermissionBits) Has(permission PermissionBits) bool {
|
|||
return p&permission != 0
|
||||
}
|
||||
|
||||
// User respresents a person.
|
||||
type AuthenticatedUser struct {
|
||||
Username string
|
||||
UsergroupLine string
|
||||
|
||||
Provider string
|
||||
SID string
|
||||
|
||||
Acls []string
|
||||
|
||||
EffectivePolicy *config.ConfigurationPolicy
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) IsGuest() bool {
|
||||
return u.Username == "guest" && u.Provider == "system"
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) parseUsergroupLine(sep string) []string {
|
||||
ret := []string{}
|
||||
|
||||
if sep != "" {
|
||||
for _, v := range strings.Split(u.UsergroupLine, sep) {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
|
||||
if trimmed != "" {
|
||||
ret = append(ret, trimmed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = strings.Fields(u.UsergroupLine)
|
||||
}
|
||||
|
||||
log.Debugf("parseUsergroupLine: %v, %v, sep:%v", u.UsergroupLine, ret, sep)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) matchesUsergroupAcl(matchUsergroups []string, sep string) bool {
|
||||
groupList := u.parseUsergroupLine(sep)
|
||||
|
||||
for _, group := range groupList {
|
||||
if slices.Contains(matchUsergroups, group) {
|
||||
log.Debugf("Usergroup %v found in %+v (len: %v)", group, groupList, len(groupList))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
func logAclNotMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
if cfg.LogDebugOptions.AclNotMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
|
|
@ -86,7 +31,7 @@ func logAclNotMatched(cfg *config.Config, aclFunction string, user *Authenticate
|
|||
}
|
||||
}
|
||||
|
||||
func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
actionTitle := "N/A"
|
||||
|
||||
if action != nil {
|
||||
|
|
@ -102,7 +47,7 @@ func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUs
|
|||
}
|
||||
}
|
||||
|
||||
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, defaultPermission bool) {
|
||||
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, defaultPermission bool) {
|
||||
if cfg.LogDebugOptions.AclNoneMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
|
|
@ -136,7 +81,7 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits
|
|||
return ret
|
||||
}
|
||||
|
||||
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action) bool {
|
||||
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
|
||||
|
||||
if cfg.LogDebugOptions.AclCheckStarted {
|
||||
|
|
@ -167,17 +112,17 @@ func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.
|
|||
}
|
||||
|
||||
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
||||
func IsAllowedLogs(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
func IsAllowedLogs(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action)
|
||||
}
|
||||
|
||||
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
||||
func IsAllowedExec(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
func IsAllowedExec(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action)
|
||||
}
|
||||
|
||||
// IsAllowedView checks if a User is allowed to view an Action
|
||||
func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
func IsAllowedView(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
if action.Hidden {
|
||||
return false
|
||||
}
|
||||
|
|
@ -185,129 +130,11 @@ func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.A
|
|||
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
|
||||
}
|
||||
|
||||
func IsAllowedKill(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
func IsAllowedKill(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action)
|
||||
}
|
||||
|
||||
func getHeaderKeyOrEmpty(headers http.Header, key string) string {
|
||||
values := headers.Values(key)
|
||||
if len(values) > 0 {
|
||||
return values[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UserFromContext tries to find a user from a Connect RPC context
|
||||
func UserFromContext[T any](ctx context.Context, req *connect.Request[T], cfg *config.Config) *AuthenticatedUser {
|
||||
user := userFromHeaders(req, cfg)
|
||||
if user.Username == "" {
|
||||
user = userFromLocalSession(req, cfg, user)
|
||||
}
|
||||
if user.Username == "" {
|
||||
user = *UserGuest(cfg)
|
||||
} else {
|
||||
buildUserAcls(cfg, &user)
|
||||
}
|
||||
|
||||
path := ""
|
||||
if req != nil {
|
||||
path = req.Spec().Procedure
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": user.Username,
|
||||
"usergroupLine": user.UsergroupLine,
|
||||
"provider": user.Provider,
|
||||
"acls": user.Acls,
|
||||
"path": path,
|
||||
}).Debugf("Authenticated API request")
|
||||
return &user
|
||||
}
|
||||
|
||||
//gocyclo:ignore
|
||||
func userFromHeaders[T any](req *connect.Request[T], cfg *config.Config) AuthenticatedUser {
|
||||
var u AuthenticatedUser
|
||||
if req == nil {
|
||||
return u
|
||||
}
|
||||
if cfg.AuthHttpHeaderUsername != "" {
|
||||
u.Username = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUsername)
|
||||
}
|
||||
if cfg.AuthHttpHeaderUserGroup != "" {
|
||||
u.UsergroupLine = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUserGroup)
|
||||
}
|
||||
if prov := getHeaderKeyOrEmpty(req.Header(), "provider"); prov != "" {
|
||||
u.Provider = prov
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
//gocyclo:ignore
|
||||
func userFromLocalSession[T any](req *connect.Request[T], cfg *config.Config, u AuthenticatedUser) AuthenticatedUser {
|
||||
if req == nil || u.Username != "" {
|
||||
return u
|
||||
}
|
||||
dummy := &http.Request{Header: req.Header()}
|
||||
c, err := dummy.Cookie("olivetin-sid-local")
|
||||
if err != nil || c == nil || c.Value == "" {
|
||||
return u
|
||||
}
|
||||
sess := auth.GetUserSession("local", c.Value)
|
||||
if sess == nil {
|
||||
log.WithFields(log.Fields{"sid": c.Value, "provider": "local"}).Warn("UserFromContext: stale local session")
|
||||
return u
|
||||
}
|
||||
if cfgUser := cfg.FindUserByUsername(sess.Username); cfgUser != nil {
|
||||
u.Username = cfgUser.Username
|
||||
u.UsergroupLine = cfgUser.Usergroup
|
||||
u.Provider = "local"
|
||||
u.SID = c.Value
|
||||
return u
|
||||
}
|
||||
log.WithFields(log.Fields{"username": sess.Username}).Warn("UserFromContext: local session user not in config")
|
||||
return u
|
||||
}
|
||||
|
||||
func UserGuest(cfg *config.Config) *AuthenticatedUser {
|
||||
ret := &AuthenticatedUser{}
|
||||
ret.Username = "guest"
|
||||
ret.UsergroupLine = "guest"
|
||||
ret.Provider = "system"
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
|
||||
ret := &AuthenticatedUser{
|
||||
Username: username,
|
||||
UsergroupLine: "system",
|
||||
Provider: "system",
|
||||
}
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if slices.Contains(acl.MatchUsernames, user.Username) {
|
||||
user.Acls = append(user.Acls, acl.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if user.matchesUsergroupAcl(acl.MatchUsergroups, cfg.AuthHttpHeaderUserGroupSep) {
|
||||
user.Acls = append(user.Acls, acl.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
user.EffectivePolicy = getEffectivePolicy(cfg, user)
|
||||
}
|
||||
|
||||
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
|
||||
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser) bool {
|
||||
if !slices.Contains(user.Acls, acl.Name) {
|
||||
// If the user does not have this ACL, then it is not relevant
|
||||
|
||||
|
|
@ -325,7 +152,7 @@ func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.
|
|||
return false
|
||||
}
|
||||
|
||||
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *AuthenticatedUser) []*config.AccessControlList {
|
||||
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *authpublic.AuthenticatedUser) []*config.AccessControlList {
|
||||
var ret []*config.AccessControlList
|
||||
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
|
|
@ -336,32 +163,3 @@ func getRelevantAcls(cfg *config.Config, actionAcls []string, user *Authenticate
|
|||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getEffectivePolicy(cfg *config.Config, user *AuthenticatedUser) *config.ConfigurationPolicy {
|
||||
ret := &config.ConfigurationPolicy{
|
||||
ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
|
||||
ShowLogList: cfg.DefaultPolicy.ShowLogList,
|
||||
}
|
||||
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if slices.Contains(user.Acls, acl.Name) {
|
||||
logAclMatched(cfg, "GetEffectivePolicy", user, nil, acl)
|
||||
|
||||
ret = buildConfigurationPolicy(ret, acl.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy {
|
||||
if policy.ShowDiagnostics {
|
||||
ret.ShowDiagnostics = policy.ShowDiagnostics
|
||||
}
|
||||
|
||||
if policy.ShowLogList {
|
||||
ret.ShowLogList = policy.ShowLogList
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
)
|
||||
|
||||
func Test_hasGroupsMatch(t *testing.T) {
|
||||
|
|
@ -49,63 +50,14 @@ func Test_hasGroupsMatch(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
user := &AuthenticatedUser{
|
||||
user := &authpublic.AuthenticatedUser{
|
||||
Username: "testuser",
|
||||
UsergroupLine: tt.usergroupLine,
|
||||
}
|
||||
|
||||
if matches := user.matchesUsergroupAcl(tt.aclMatchUsergroups, tt.sep); matches != tt.matches {
|
||||
t.Errorf("AuthenticatedUser.matchesUsergroupAcl() = %v, want %v for usergroups %v", matches, tt.matches, tt.aclMatchUsergroups)
|
||||
if matches := user.MatchesUsergroupAcl(tt.aclMatchUsergroups, tt.sep); matches != tt.matches {
|
||||
t.Errorf("AuthenticatedUser.MatchesUsergroupAcl() = %v, want %v for usergroups %v", matches, tt.matches, tt.aclMatchUsergroups)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseUsergroupLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
usergroupLine string
|
||||
expectedGroups []string
|
||||
sep string
|
||||
}{
|
||||
{
|
||||
name: "Default separator (space)",
|
||||
usergroupLine: "group1 group2",
|
||||
expectedGroups: []string{"group1", "group2"},
|
||||
},
|
||||
{
|
||||
name: "Comma-separated groups",
|
||||
usergroupLine: "group1 , group2",
|
||||
expectedGroups: []string{"group1", "group2"},
|
||||
sep: ",",
|
||||
},
|
||||
{
|
||||
name: "Multiple spaces",
|
||||
usergroupLine: "group1 , group2 , group3",
|
||||
expectedGroups: []string{"group1", "group2", "group3"},
|
||||
sep: ",",
|
||||
},
|
||||
{
|
||||
name: "Empty usergroup line",
|
||||
usergroupLine: "",
|
||||
expectedGroups: []string{},
|
||||
},
|
||||
{
|
||||
name: "Empty group names",
|
||||
usergroupLine: "|group1| | group3|",
|
||||
expectedGroups: []string{"group1", "group3"},
|
||||
sep: "|",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
user := &AuthenticatedUser{
|
||||
Username: "testuser",
|
||||
UsergroupLine: tt.usergroupLine,
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedGroups, user.parseUsergroupLine(tt.sep))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
ctx "context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
|
@ -18,6 +19,7 @@ import (
|
|||
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
auth "github.com/OliveTin/OliveTin/internal/auth"
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||
|
|
@ -50,7 +52,7 @@ func (api *oliveTinAPI) copyOfStreamingClients() []*streamingClient {
|
|||
|
||||
type streamingClient struct {
|
||||
channel chan *apiv1.EventStreamResponse
|
||||
AuthenticatedUser *acl.AuthenticatedUser
|
||||
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
|
||||
|
|
@ -77,17 +79,18 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.K
|
|||
return connect.NewResponse(ret), nil
|
||||
}
|
||||
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
api.killActionByTrackingId(user, action, execReqLogEntry, ret)
|
||||
|
||||
return connect.NewResponse(ret), nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
|
||||
func (api *oliveTinAPI) killActionByTrackingId(user *authpublic.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
|
||||
if !acl.IsAllowedKill(api.cfg, user, action) {
|
||||
log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
|
||||
ret.Killed = false
|
||||
return
|
||||
}
|
||||
|
||||
err := api.executor.Kill(execReqLogEntry)
|
||||
|
|
@ -114,7 +117,7 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.
|
|||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId))
|
||||
}
|
||||
|
||||
authenticatedUser := acl.UserFromContext(ctx, req, api.cfg)
|
||||
authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Binding: pair,
|
||||
|
|
@ -203,7 +206,7 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request
|
|||
args[arg.Name] = arg.Value
|
||||
}
|
||||
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
||||
|
|
@ -234,7 +237,7 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a
|
|||
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
||||
TrackingID: uuid.NewString(),
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromContext(ctx, req, api.cfg),
|
||||
AuthenticatedUser: auth.UserFromApiCall(ctx, req, api.cfg),
|
||||
Cfg: api.cfg,
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +251,7 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a
|
|||
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
||||
|
|
@ -272,7 +275,7 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re
|
|||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry {
|
||||
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
|
||||
pble := &apiv1.LogEntry{
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
|
|
@ -326,7 +329,7 @@ func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *execut
|
|||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
|
||||
res := &apiv1.ExecutionStatusResponse{}
|
||||
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -351,7 +354,7 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": user.Username,
|
||||
|
|
@ -374,7 +377,7 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -396,7 +399,7 @@ func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[a
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -411,14 +414,14 @@ func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1
|
|||
return api.buildCustomDashboardResponse(dashboardRenderRequest, req.Msg.Title)
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) checkDashboardAccess(user *acl.AuthenticatedUser) error {
|
||||
func (api *oliveTinAPI) checkDashboardAccess(user *authpublic.AuthenticatedUser) error {
|
||||
if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
|
||||
return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) createDashboardRenderRequest(user *acl.AuthenticatedUser) *DashboardRenderRequest {
|
||||
func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser) *DashboardRenderRequest {
|
||||
return &DashboardRenderRequest{
|
||||
AuthenticatedUser: user,
|
||||
cfg: api.cfg,
|
||||
|
|
@ -446,7 +449,7 @@ func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest,
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -470,7 +473,7 @@ func isValidLogEntry(e *executor.InternalLogEntry) bool {
|
|||
}
|
||||
|
||||
// isLogEntryAllowed checks if a log entry is allowed to be viewed by the user.
|
||||
func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *acl.AuthenticatedUser) bool {
|
||||
func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
|
||||
return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
|
||||
}
|
||||
|
||||
|
|
@ -498,7 +501,7 @@ func calculateReversedIndices(page pageInfo, filteredLen int) (int64, int64) {
|
|||
}
|
||||
|
||||
// buildActionLogsResponse builds the response with paginated log entries.
|
||||
func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *acl.AuthenticatedUser) *apiv1.GetActionLogsResponse {
|
||||
func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *authpublic.AuthenticatedUser) *apiv1.GetActionLogsResponse {
|
||||
startIdx, endIdx := calculateReversedIndices(page, len(filtered))
|
||||
ret := &apiv1.GetActionLogsResponse{}
|
||||
for _, le := range filtered[startIdx:endIdx] {
|
||||
|
|
@ -512,7 +515,7 @@ func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLog
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv1.GetActionLogsRequest]) (*connect.Response[apiv1.GetActionLogsResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -527,20 +530,7 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
|
|||
return connect.NewResponse(api.buildActionLogsResponse(filtered, page, user)), nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) pbLogsFiltered(entries []*executor.InternalLogEntry, user *acl.AuthenticatedUser) []*apiv1.LogEntry {
|
||||
out := make([]*apiv1.LogEntry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if !isValidLogEntry(e) {
|
||||
continue
|
||||
}
|
||||
if api.isLogEntryAllowed(e, user) {
|
||||
out = append(out, api.internalLogEntryToPb(e, user))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) filterLogsByACL(entries []*executor.InternalLogEntry, user *acl.AuthenticatedUser) []*executor.InternalLogEntry {
|
||||
func (api *oliveTinAPI) filterLogsByACL(entries []*executor.InternalLogEntry, user *authpublic.AuthenticatedUser) []*executor.InternalLogEntry {
|
||||
filtered := make([]*executor.InternalLogEntry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if !isValidLogEntry(e) {
|
||||
|
|
@ -595,7 +585,7 @@ func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Reque
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -681,7 +671,7 @@ func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.Ge
|
|||
func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
|
||||
log.Debugf("EventStream: %v", req.Msg)
|
||||
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return err
|
||||
|
|
@ -800,7 +790,7 @@ func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[api
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitRequest]) (*connect.Response[apiv1.InitResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
|
||||
|
||||
|
|
@ -833,7 +823,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
|
|||
return connect.NewResponse(res), nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) buildRootDashboards(user *acl.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
||||
func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
||||
var rootDashboards []string
|
||||
dashboardRenderRequest := api.createDashboardRenderRequest(user)
|
||||
|
||||
|
|
@ -865,14 +855,18 @@ func (api *oliveTinAPI) addCustomDashboards(rootDashboards *[]string, dashboards
|
|||
func buildPublicOAuth2ProvidersList(cfg *config.Config) []*apiv1.OAuth2Provider {
|
||||
var publicProviders []*apiv1.OAuth2Provider
|
||||
|
||||
for _, provider := range cfg.AuthOAuth2Providers {
|
||||
for providerKey, provider := range cfg.AuthOAuth2Providers {
|
||||
publicProviders = append(publicProviders, &apiv1.OAuth2Provider{
|
||||
Title: provider.Title,
|
||||
Url: provider.AuthUrl,
|
||||
Icon: provider.Icon,
|
||||
Key: providerKey,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(publicProviders, func(i, j int) bool {
|
||||
return publicProviders[i].Key < publicProviders[j].Key
|
||||
})
|
||||
|
||||
return publicProviders
|
||||
}
|
||||
|
||||
|
|
@ -914,38 +908,55 @@ func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.GetEntitiesRequest]) (*connect.Response[apiv1.GetEntitiesResponse], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &apiv1.GetEntitiesResponse{
|
||||
EntityDefinitions: make([]*apiv1.EntityDefinition, 0),
|
||||
entityMap := entities.GetEntities()
|
||||
entityNames := make([]string, 0, len(entityMap))
|
||||
for name := range entityMap {
|
||||
entityNames = append(entityNames, name)
|
||||
}
|
||||
sort.Strings(entityNames)
|
||||
|
||||
for name, entityInstances := range entities.GetEntities() {
|
||||
entityDefinitions := make([]*apiv1.EntityDefinition, 0, len(entityNames))
|
||||
for _, name := range entityNames {
|
||||
def := &apiv1.EntityDefinition{
|
||||
Title: name,
|
||||
UsedOnDashboards: findDashboardsForEntity(name, api.cfg.Dashboards),
|
||||
Instances: buildSortedEntityInstances(name, entityMap[name]),
|
||||
}
|
||||
entityDefinitions = append(entityDefinitions, def)
|
||||
}
|
||||
|
||||
for _, e := range entityInstances {
|
||||
entity := &apiv1.Entity{
|
||||
Title: e.Title,
|
||||
UniqueKey: e.UniqueKey,
|
||||
Type: name,
|
||||
}
|
||||
|
||||
def.Instances = append(def.Instances, entity)
|
||||
}
|
||||
|
||||
res.EntityDefinitions = append(res.EntityDefinitions, def)
|
||||
res := &apiv1.GetEntitiesResponse{
|
||||
EntityDefinitions: entityDefinitions,
|
||||
}
|
||||
|
||||
return connect.NewResponse(res), nil
|
||||
}
|
||||
|
||||
func buildSortedEntityInstances(entityType string, entityInstances map[string]*entities.Entity) []*apiv1.Entity {
|
||||
instanceKeys := make([]string, 0, len(entityInstances))
|
||||
for key := range entityInstances {
|
||||
instanceKeys = append(instanceKeys, key)
|
||||
}
|
||||
sort.Strings(instanceKeys)
|
||||
|
||||
instances := make([]*apiv1.Entity, 0, len(instanceKeys))
|
||||
for _, key := range instanceKeys {
|
||||
e := entityInstances[key]
|
||||
instances = append(instances, &apiv1.Entity{
|
||||
Title: e.Title,
|
||||
UniqueKey: e.UniqueKey,
|
||||
Type: entityType,
|
||||
})
|
||||
}
|
||||
return instances
|
||||
}
|
||||
|
||||
func findDashboardsForEntity(entityTitle string, dashboards []*config.DashboardComponent) []string {
|
||||
var foundDashboards []string
|
||||
|
||||
|
|
@ -967,7 +978,7 @@ func findEntityInComponents(entityTitle string, parentTitle string, components [
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
|
||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package api
|
|||
import (
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||
)
|
||||
|
||||
type DashboardRenderRequest struct {
|
||||
AuthenticatedUser *acl.AuthenticatedUser
|
||||
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||
cfg *config.Config
|
||||
ex *executor.Executor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||
apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/entities"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
|
||||
"net/http"
|
||||
|
|
@ -93,3 +94,82 @@ func TestGetActionsAndStart(t *testing.T) {
|
|||
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
func TestGetEntities(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
|
||||
ts, client := getNewTestServerAndClient(t, cfg)
|
||||
defer ts.Close()
|
||||
|
||||
setupTestEntities()
|
||||
|
||||
resp, err := client.GetEntities(context.Background(), connect.NewRequest(&apiv1.GetEntitiesRequest{}))
|
||||
|
||||
assert.NoError(t, err, "GetEntities should not return an error")
|
||||
assert.NotNil(t, resp, "GetEntities response should not be nil")
|
||||
assert.NotNil(t, resp.Msg, "GetEntities response message should not be nil")
|
||||
|
||||
entityDefinitions := resp.Msg.EntityDefinitions
|
||||
assert.Equal(t, 3, len(entityDefinitions), "Should return 3 entity definitions")
|
||||
|
||||
validateEntityOrderAndStructure(t, entityDefinitions)
|
||||
validateNoDuplicates(t, entityDefinitions)
|
||||
validateConsistency(t, client, entityDefinitions)
|
||||
}
|
||||
|
||||
func setupTestEntities() {
|
||||
entities.ClearEntities("server")
|
||||
entities.ClearEntities("database")
|
||||
entities.ClearEntities("application")
|
||||
|
||||
entities.AddEntity("server", "zebra", map[string]any{"title": "Server Zebra", "hostname": "zebra.example.com"})
|
||||
entities.AddEntity("server", "alpha", map[string]any{"title": "Server Alpha", "hostname": "alpha.example.com"})
|
||||
entities.AddEntity("server", "beta", map[string]any{"title": "Server Beta", "hostname": "beta.example.com"})
|
||||
|
||||
entities.AddEntity("database", "mysql", map[string]any{"title": "MySQL Database", "type": "mysql"})
|
||||
entities.AddEntity("database", "postgres", map[string]any{"title": "PostgreSQL Database", "type": "postgres"})
|
||||
|
||||
entities.AddEntity("application", "webapp", map[string]any{"title": "Web Application", "port": 8080})
|
||||
}
|
||||
|
||||
func validateEntityOrderAndStructure(t *testing.T, entityDefinitions []*apiv1.EntityDefinition) {
|
||||
assert.Equal(t, "application", entityDefinitions[0].Title, "First entity should be 'application' (alphabetically first)")
|
||||
assert.Equal(t, 1, len(entityDefinitions[0].Instances), "Application should have 1 instance")
|
||||
assert.Equal(t, "webapp", entityDefinitions[0].Instances[0].UniqueKey, "Application instance should be 'webapp'")
|
||||
|
||||
assert.Equal(t, "database", entityDefinitions[1].Title, "Second entity should be 'database' (alphabetically second)")
|
||||
assert.Equal(t, 2, len(entityDefinitions[1].Instances), "Database should have 2 instances")
|
||||
assert.Equal(t, "mysql", entityDefinitions[1].Instances[0].UniqueKey, "First database instance should be 'mysql' (alphabetically first)")
|
||||
assert.Equal(t, "postgres", entityDefinitions[1].Instances[1].UniqueKey, "Second database instance should be 'postgres' (alphabetically second)")
|
||||
|
||||
assert.Equal(t, "server", entityDefinitions[2].Title, "Third entity should be 'server' (alphabetically third)")
|
||||
assert.Equal(t, 3, len(entityDefinitions[2].Instances), "Server should have 3 instances")
|
||||
assert.Equal(t, "alpha", entityDefinitions[2].Instances[0].UniqueKey, "First server instance should be 'alpha' (alphabetically first)")
|
||||
assert.Equal(t, "beta", entityDefinitions[2].Instances[1].UniqueKey, "Second server instance should be 'beta' (alphabetically second)")
|
||||
assert.Equal(t, "zebra", entityDefinitions[2].Instances[2].UniqueKey, "Third server instance should be 'zebra' (alphabetically third)")
|
||||
}
|
||||
|
||||
func validateNoDuplicates(t *testing.T, entityDefinitions []*apiv1.EntityDefinition) {
|
||||
instanceKeys := make(map[string]map[string]bool)
|
||||
for _, def := range entityDefinitions {
|
||||
instanceKeys[def.Title] = make(map[string]bool)
|
||||
for _, inst := range def.Instances {
|
||||
assert.False(t, instanceKeys[def.Title][inst.UniqueKey], "Instance key %s should not be duplicated in entity %s", inst.UniqueKey, def.Title)
|
||||
instanceKeys[def.Title][inst.UniqueKey] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateConsistency(t *testing.T, client apiv1connect.OliveTinApiServiceClient, entityDefinitions []*apiv1.EntityDefinition) {
|
||||
resp2, err2 := client.GetEntities(context.Background(), connect.NewRequest(&apiv1.GetEntitiesRequest{}))
|
||||
assert.NoError(t, err2, "Second GetEntities call should not return an error")
|
||||
assert.Equal(t, len(entityDefinitions), len(resp2.Msg.EntityDefinitions), "Second call should return same number of entity definitions")
|
||||
|
||||
for i, def := range entityDefinitions {
|
||||
assert.Equal(t, def.Title, resp2.Msg.EntityDefinitions[i].Title, "Entity order should be consistent across calls")
|
||||
assert.Equal(t, len(def.Instances), len(resp2.Msg.EntityDefinitions[i].Instances), "Instance count should be consistent")
|
||||
for j, inst := range def.Instances {
|
||||
assert.Equal(t, inst.UniqueKey, resp2.Msg.EntityDefinitions[i].Instances[j].UniqueKey, "Instance order should be consistent across calls")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/alexedwards/argon2id"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var defaultParams = argon2id.Params{
|
||||
|
|
@ -15,7 +16,7 @@ var defaultParams = argon2id.Params{
|
|||
KeyLength: 32,
|
||||
}
|
||||
|
||||
func createHash(password string) (string, error) {
|
||||
func CreateHash(password string) (string, error) {
|
||||
hash, err := argon2id.CreateHash(password, &defaultParams)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -26,6 +27,10 @@ func createHash(password string) (string, error) {
|
|||
return hash, nil
|
||||
}
|
||||
|
||||
func createHash(password string) (string, error) {
|
||||
return CreateHash(password)
|
||||
}
|
||||
|
||||
func comparePasswordAndHash(password, hash string) bool {
|
||||
match, err := argon2id.ComparePasswordAndHash(password, hash)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
types "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
otjwt "github.com/OliveTin/OliveTin/internal/auth/otjwt"
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var authChain = []func(*types.AuthCheckingContext) *types.AuthenticatedUser{
|
||||
checkUserFromHeaders,
|
||||
checkUserFromLocalSession,
|
||||
otjwt.CheckUserFromJwtHeader,
|
||||
otjwt.CheckUserFromJwtCookie,
|
||||
}
|
||||
|
||||
// Handlers like the OAuth2's handler are "instance methods", so they need to be added to the auth chain after the other handlers.
|
||||
func AddAuthChainFunction(check func(*types.AuthCheckingContext) *types.AuthenticatedUser) {
|
||||
authChain = append(authChain, check)
|
||||
}
|
||||
|
||||
func runAuthChain[T any](req *connect.Request[T], cfg *config.Config) *types.AuthenticatedUser {
|
||||
var user *types.AuthenticatedUser
|
||||
|
||||
authCtx := &types.AuthCheckingContext{
|
||||
Request: &http.Request{Header: req.Header()},
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
for _, check := range authChain {
|
||||
user = check(authCtx)
|
||||
|
||||
if user != nil && user.Username != "" {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UserFromApiCall[T any](ctx context.Context, req *connect.Request[T], cfg *config.Config) *types.AuthenticatedUser {
|
||||
user := runAuthChain(req, cfg)
|
||||
|
||||
log.Tracef("UserFromApiCall Context: %+v", ctx)
|
||||
|
||||
if user == nil || user.Username == "" {
|
||||
user = UserGuest(cfg)
|
||||
} else {
|
||||
user.BuildUserAcls(cfg)
|
||||
}
|
||||
|
||||
path := ""
|
||||
if req != nil {
|
||||
path = req.Spec().Procedure
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": user.Username,
|
||||
"usergroupLine": user.UsergroupLine,
|
||||
"provider": user.Provider,
|
||||
"acls": user.Acls,
|
||||
"path": path,
|
||||
}).Debugf("Authenticated API request")
|
||||
|
||||
return user
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package authpublic
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// User represents a person.
|
||||
type AuthenticatedUser struct {
|
||||
Username string
|
||||
UsergroupLine string
|
||||
|
||||
Provider string
|
||||
SID string
|
||||
|
||||
Acls []string
|
||||
|
||||
EffectivePolicy *config.ConfigurationPolicy
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) IsGuest() bool {
|
||||
return u.Username == "guest" && u.Provider == "system"
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) parseUsergroupLine(sep string) []string {
|
||||
ret := []string{}
|
||||
|
||||
if sep != "" {
|
||||
for _, v := range strings.Split(u.UsergroupLine, sep) {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
|
||||
if trimmed != "" {
|
||||
ret = append(ret, trimmed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = strings.Fields(u.UsergroupLine)
|
||||
}
|
||||
|
||||
log.Debugf("parseUsergroupLine: %v, %v, sep:%v", u.UsergroupLine, ret, sep)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) MatchesUsergroupAcl(matchUsergroups []string, sep string) bool {
|
||||
groupList := u.parseUsergroupLine(sep)
|
||||
|
||||
for _, group := range groupList {
|
||||
if slices.Contains(matchUsergroups, group) {
|
||||
log.Debugf("Usergroup %v found in %+v (len: %v)", group, groupList, len(groupList))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) BuildUserAcls(cfg *config.Config) {
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if slices.Contains(acl.MatchUsernames, u.Username) {
|
||||
u.Acls = append(u.Acls, acl.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if u.MatchesUsergroupAcl(acl.MatchUsergroups, cfg.AuthHttpHeaderUserGroupSep) {
|
||||
u.Acls = append(u.Acls, acl.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
u.EffectivePolicy = getEffectivePolicy(cfg, u)
|
||||
}
|
||||
|
||||
func getEffectivePolicy(cfg *config.Config, u *AuthenticatedUser) *config.ConfigurationPolicy {
|
||||
ret := &config.ConfigurationPolicy{
|
||||
ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
|
||||
ShowLogList: cfg.DefaultPolicy.ShowLogList,
|
||||
}
|
||||
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if slices.Contains(u.Acls, acl.Name) {
|
||||
ret = buildConfigurationPolicy(ret, acl.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy {
|
||||
if policy.ShowDiagnostics {
|
||||
ret.ShowDiagnostics = policy.ShowDiagnostics
|
||||
}
|
||||
|
||||
if policy.ShowLogList {
|
||||
ret.ShowLogList = policy.ShowLogList
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package authpublic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseUsergroupLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
usergroupLine string
|
||||
expectedGroups []string
|
||||
sep string
|
||||
}{
|
||||
{
|
||||
name: "Default separator (space)",
|
||||
usergroupLine: "group1 group2",
|
||||
expectedGroups: []string{"group1", "group2"},
|
||||
},
|
||||
{
|
||||
name: "Comma-separated groups",
|
||||
usergroupLine: "group1 , group2",
|
||||
expectedGroups: []string{"group1", "group2"},
|
||||
sep: ",",
|
||||
},
|
||||
{
|
||||
name: "Multiple spaces",
|
||||
usergroupLine: "group1 , group2 , group3",
|
||||
expectedGroups: []string{"group1", "group2", "group3"},
|
||||
sep: ",",
|
||||
},
|
||||
{
|
||||
name: "Empty usergroup line",
|
||||
usergroupLine: "",
|
||||
expectedGroups: []string{},
|
||||
},
|
||||
{
|
||||
name: "Empty group names",
|
||||
usergroupLine: "|group1| | group3|",
|
||||
expectedGroups: []string{"group1", "group3"},
|
||||
sep: "|",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
user := &AuthenticatedUser{
|
||||
Username: "testuser",
|
||||
UsergroupLine: tt.usergroupLine,
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedGroups, user.parseUsergroupLine(tt.sep))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package authpublic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
)
|
||||
|
||||
type AuthCheckingContext struct {
|
||||
Config *config.Config
|
||||
Request *http.Request
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
types "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func getLocalSessionCookie(r *http.Request) (string, bool) {
|
||||
c, err := r.Cookie("olivetin-sid-local")
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if c == nil {
|
||||
return "", false
|
||||
}
|
||||
if c.Value == "" {
|
||||
return "", false
|
||||
}
|
||||
return c.Value, true
|
||||
}
|
||||
|
||||
func checkUserFromLocalSession(context *types.AuthCheckingContext) *types.AuthenticatedUser {
|
||||
u := &types.AuthenticatedUser{}
|
||||
|
||||
sid, ok := getLocalSessionCookie(context.Request)
|
||||
if !ok {
|
||||
return u
|
||||
}
|
||||
|
||||
sess := GetUserSession("local", sid)
|
||||
if sess == nil {
|
||||
log.WithFields(log.Fields{"sid": sid, "provider": "local"}).Warn("UserFromContext: stale local session")
|
||||
return u
|
||||
}
|
||||
|
||||
cfgUser := context.Config.FindUserByUsername(sess.Username)
|
||||
if cfgUser == nil {
|
||||
log.WithFields(log.Fields{"username": sess.Username}).Warn("UserFromContext: local session user not in config")
|
||||
return u
|
||||
}
|
||||
|
||||
u.Username = cfgUser.Username
|
||||
u.UsergroupLine = cfgUser.Usergroup
|
||||
u.Provider = "local"
|
||||
u.SID = sid
|
||||
return u
|
||||
}
|
||||
|
|
@ -1,73 +1,144 @@
|
|||
package httpservers
|
||||
package otjwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
|
||||
// "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/MicahParks/keyfunc/v3"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/MicahParks/keyfunc/v3"
|
||||
authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||
if cfg.AuthJwtCertsURL != "" {
|
||||
return parseJwtTokenWithRemoteKey(cfg, jwtString)
|
||||
}
|
||||
|
||||
if cfg.AuthJwtPubKeyPath != "" {
|
||||
return parseJwtTokenWithLocalKey(cfg, jwtString)
|
||||
}
|
||||
|
||||
if cfg.AuthJwtHmacSecret == "" {
|
||||
return nil, errors.New("no JWT authentication method configured")
|
||||
}
|
||||
|
||||
return parseJwtTokenWithHMAC(cfg, jwtString)
|
||||
}
|
||||
|
||||
func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
|
||||
token, err := parseJwtToken(cfg, jwtString)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("jwt parse failure: %v", err)
|
||||
return nil, errors.New("jwt parse failure")
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
} else {
|
||||
return nil, errors.New("jwt token isn't valid")
|
||||
}
|
||||
}
|
||||
|
||||
func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token, error) {
|
||||
err := initJwks(cfg)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("jwt init JWKS failure: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
|
||||
}
|
||||
|
||||
var (
|
||||
pubKeyBytes []byte = nil
|
||||
pubKey *rsa.PublicKey
|
||||
loadedKeyPath string
|
||||
|
||||
jwksVerifier keyfunc.Keyfunc
|
||||
jwksOnce sync.Once
|
||||
jwksInitErr error
|
||||
|
||||
localKeyMutex sync.RWMutex
|
||||
localKeyInitErr error
|
||||
)
|
||||
|
||||
func initJwks(cfg *config.Config) {
|
||||
if jwksVerifier == nil {
|
||||
var err error
|
||||
|
||||
func initJwks(cfg *config.Config) error {
|
||||
jwksOnce.Do(func() {
|
||||
if cfg.AuthJwtCertsURL != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
jwksVerifier, err = keyfunc.NewDefaultCtx(ctx, []string{
|
||||
cfg.AuthJwtCertsURL,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Init JWKS Failure: %v", err)
|
||||
jwksInitErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
return jwksInitErr
|
||||
}
|
||||
|
||||
defer cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readLocalPublicKey(cfg *config.Config) error {
|
||||
if pubKeyBytes != nil {
|
||||
return nil // Already read.
|
||||
}
|
||||
|
||||
pubKeyBytes, err := os.ReadFile(cfg.AuthJwtPubKeyPath)
|
||||
func loadPublicKeyFromFile(keyPath string) error {
|
||||
keyBytes, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't read public key from file %s", cfg.AuthJwtPubKeyPath)
|
||||
return fmt.Errorf("couldn't read public key from file %s", keyPath)
|
||||
}
|
||||
|
||||
// Since the token is RSA (which we validated at the start of this function), the return type of this function actually has to be rsa.PublicKey!
|
||||
pubKey, err = jwt.ParseRSAPublicKeyFromPEM(pubKeyBytes)
|
||||
parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(keyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing public key object (from %s)", cfg.AuthJwtPubKeyPath)
|
||||
return fmt.Errorf("error parsing public key object (from %s)", keyPath)
|
||||
}
|
||||
|
||||
pubKeyBytes = keyBytes
|
||||
pubKey = parsedKey
|
||||
loadedKeyPath = keyPath
|
||||
localKeyInitErr = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token, error) {
|
||||
initJwks(cfg)
|
||||
func isKeyLoadedForPath(keyPath string) bool {
|
||||
return pubKeyBytes != nil && loadedKeyPath == keyPath
|
||||
}
|
||||
|
||||
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
|
||||
func readLocalPublicKeyWithLock(keyPath string) error {
|
||||
localKeyMutex.RLock()
|
||||
alreadyLoaded := isKeyLoadedForPath(keyPath)
|
||||
localKeyMutex.RUnlock()
|
||||
|
||||
if alreadyLoaded {
|
||||
return nil
|
||||
}
|
||||
|
||||
localKeyMutex.Lock()
|
||||
defer localKeyMutex.Unlock()
|
||||
|
||||
if isKeyLoadedForPath(keyPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
localKeyInitErr = loadPublicKeyFromFile(keyPath)
|
||||
return localKeyInitErr
|
||||
}
|
||||
|
||||
func readLocalPublicKey(cfg *config.Config) error {
|
||||
if cfg.AuthJwtPubKeyPath == "" {
|
||||
return errors.New("no JWT public key path configured")
|
||||
}
|
||||
return readLocalPublicKeyWithLock(cfg.AuthJwtPubKeyPath)
|
||||
}
|
||||
|
||||
func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||
|
|
@ -97,33 +168,6 @@ func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, er
|
|||
})
|
||||
}
|
||||
|
||||
func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||
if cfg.AuthJwtCertsURL != "" {
|
||||
return parseJwtTokenWithRemoteKey(cfg, jwtString)
|
||||
}
|
||||
|
||||
if cfg.AuthJwtPubKeyPath != "" {
|
||||
return parseJwtTokenWithLocalKey(cfg, jwtString)
|
||||
}
|
||||
|
||||
return parseJwtTokenWithHMAC(cfg, jwtString)
|
||||
}
|
||||
|
||||
func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
|
||||
token, err := parseJwtToken(cfg, jwtString)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("jwt parse failure: %v", err)
|
||||
return nil, errors.New("jwt parse failure")
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
} else {
|
||||
return nil, errors.New("jwt token isn't valid")
|
||||
}
|
||||
}
|
||||
|
||||
func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string {
|
||||
if val, ok := claims[key]; ok {
|
||||
return fmt.Sprintf("%s", val)
|
||||
|
|
@ -132,33 +176,48 @@ func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) str
|
|||
}
|
||||
}
|
||||
|
||||
func parseJwtCookie(cfg *config.Config, request *http.Request) (string, string) {
|
||||
cookie, err := request.Cookie(cfg.AuthJwtCookieName)
|
||||
func CheckUserFromJwtCookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
||||
cookie, err := context.Request.Cookie(context.Config.AuthJwtCookieName)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("jwt cookie check %v name: %v", err, cfg.AuthJwtCookieName)
|
||||
return "", ""
|
||||
log.Debugf("jwt cookie check %v name: %v", err, context.Config.AuthJwtCookieName)
|
||||
return nil
|
||||
}
|
||||
|
||||
return parseJwt(cfg, cookie.Value)
|
||||
return parseJwt(context.Config, cookie.Value)
|
||||
}
|
||||
|
||||
func parseJwt(cfg *config.Config, token string) (string, string) {
|
||||
func CheckUserFromJwtHeader(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
||||
header := context.Request.Header.Get(context.Config.AuthJwtHeader)
|
||||
if header == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(header, "Bearer ")
|
||||
token = strings.TrimSpace(token)
|
||||
|
||||
return parseJwt(context.Config, token)
|
||||
}
|
||||
|
||||
func parseJwt(cfg *config.Config, token string) *authTypes.AuthenticatedUser {
|
||||
claims, err := getClaimsFromJwtToken(cfg, token)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("jwt claim error: %+v", err)
|
||||
return "", ""
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.InsecureAllowDumpJwtClaims {
|
||||
log.Debugf("JWT Claims %+v", claims)
|
||||
}
|
||||
|
||||
username := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, "")
|
||||
usergroup := parseGroupClaim(cfg.AuthJwtClaimUserGroup, claims)
|
||||
user := &authTypes.AuthenticatedUser{
|
||||
Username: lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, ""),
|
||||
UsergroupLine: parseGroupClaim(cfg.AuthJwtClaimUserGroup, claims),
|
||||
Provider: "jwt",
|
||||
}
|
||||
|
||||
return username, usergroup
|
||||
return user
|
||||
}
|
||||
|
||||
func parseGroupClaim(groupClaim string, claims jwt.MapClaims) string {
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package otjwt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func generateRSAKeyPair(t *testing.T) (*rsa.PrivateKey, []byte) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate RSA key: %v", err)
|
||||
}
|
||||
|
||||
pubKey := &privateKey.PublicKey
|
||||
pkixPubKey, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal public key: %v", err)
|
||||
}
|
||||
|
||||
pubPem := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: pkixPubKey,
|
||||
},
|
||||
)
|
||||
|
||||
return privateKey, pubPem
|
||||
}
|
||||
|
||||
func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
|
||||
tmpFile, err := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
t.Logf("Created File: %s", tmpFile.Name())
|
||||
|
||||
privateKey, pubPem := generateRSAKeyPair(t)
|
||||
|
||||
if err := os.WriteFile(tmpFile.Name(), pubPem, 0644); err != nil {
|
||||
t.Fatalf("error when dumping pubKey: %s \n", err)
|
||||
}
|
||||
|
||||
return privateKey, tmpFile.Name()
|
||||
}
|
||||
|
||||
func newMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func createJWTTokenWithExpiration(t *testing.T, privateKey *rsa.PrivateKey, expire int64) string {
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["nbf"] = time.Now().Unix() - 1000
|
||||
claims["exp"] = time.Now().Unix() + expire
|
||||
claims["sub"] = "test"
|
||||
claims["olivetinGroup"] = "test"
|
||||
|
||||
tokenStr, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign JWT token: %v", err)
|
||||
}
|
||||
return tokenStr
|
||||
}
|
||||
|
||||
func setupJWTTestHandler(t *testing.T, cfg *config.Config) http.Handler {
|
||||
mux := newMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
context := &authpublic.AuthCheckingContext{
|
||||
Request: r,
|
||||
Config: cfg,
|
||||
}
|
||||
user := CheckUserFromJwtHeader(context)
|
||||
|
||||
if user == nil {
|
||||
w.WriteHeader(403)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "test", user.Username)
|
||||
assert.Equal(t, "test", user.UsergroupLine)
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func verifyJWTResponse(t *testing.T, res *http.Response, expectCode int) {
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, expectCode, res.StatusCode)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
t.Logf("Response body: %s", string(body))
|
||||
}
|
||||
|
||||
func testJwkValidation(t *testing.T, expire int64, expectCode int) {
|
||||
privateKey, publicKeyPath := createKeys(t)
|
||||
defer os.Remove(publicKeyPath)
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||
cfg.AuthJwtClaimUsername = "sub"
|
||||
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
||||
cfg.AuthJwtHeader = "Authorization"
|
||||
|
||||
tokenStr := createJWTTokenWithExpiration(t, privateKey, expire)
|
||||
handler := setupJWTTestHandler(t, cfg)
|
||||
|
||||
srv := httptest.NewServer(handler)
|
||||
defer srv.Close()
|
||||
|
||||
res := makeJWTRequest(t, srv, tokenStr)
|
||||
verifyJWTResponse(t, res, expectCode)
|
||||
}
|
||||
|
||||
func TestJWTSignatureVerificationSucceeds(t *testing.T) {
|
||||
testJwkValidation(t, 1000, 200)
|
||||
}
|
||||
|
||||
func TestJWTSignatureVerificationFails(t *testing.T) {
|
||||
testJwkValidation(t, -500, 403)
|
||||
}
|
||||
|
||||
func createJWTTokenWithGroups(t *testing.T, privateKey *rsa.PrivateKey, groups interface{}) string {
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["nbf"] = time.Now().Unix() - 1000
|
||||
claims["exp"] = time.Now().Unix() + 2000
|
||||
claims["sub"] = "test"
|
||||
claims["olivetinGroup"] = groups
|
||||
|
||||
tokenStr, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign JWT token: %v", err)
|
||||
}
|
||||
return tokenStr
|
||||
}
|
||||
|
||||
func makeJWTRequest(t *testing.T, srv *httptest.Server, tokenStr string) *http.Response {
|
||||
req, err := http.NewRequest("GET", srv.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+tokenStr)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Client err: %+v", err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestJWTHeader(t *testing.T) {
|
||||
privateKey, publicKeyPath := createKeys(t)
|
||||
defer os.Remove(publicKeyPath)
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||
cfg.AuthJwtClaimUsername = "sub"
|
||||
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
||||
cfg.AuthJwtHeader = "Authorization"
|
||||
|
||||
tokenStr := createJWTTokenWithGroups(t, privateKey, []string{"test", "test2"})
|
||||
|
||||
mux := newMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
context := &authpublic.AuthCheckingContext{
|
||||
Request: r,
|
||||
Config: cfg,
|
||||
}
|
||||
user := CheckUserFromJwtHeader(context)
|
||||
|
||||
if user == nil {
|
||||
w.WriteHeader(403)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "test", user.Username)
|
||||
assert.Equal(t, "test test2", user.UsergroupLine)
|
||||
})
|
||||
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
res := makeJWTRequest(t, srv, tokenStr)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, res.StatusCode)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
t.Logf("Response body: %s", string(body))
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package httpservers
|
||||
package otoauth2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -8,13 +8,15 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type OAuth2Handler struct {
|
||||
|
|
@ -110,7 +112,7 @@ func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Re
|
|||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
MaxAge: 31556952, // 1 year
|
||||
MaxAge: 900, // 15 minutes
|
||||
Secure: r.TLS != nil,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
|
|
@ -119,7 +121,7 @@ func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Re
|
|||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
func (h *OAuth2Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := randString(16)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -149,30 +151,31 @@ func (h *OAuth2Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request)
|
|||
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
|
||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||
state := cookie.Value
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get state cookie: %v", err)
|
||||
|
||||
http.Error(w, "State not found", http.StatusBadRequest)
|
||||
return nil, state, false
|
||||
func (h *OAuth2Handler) validateStateMatch(queryState, cookieState string) bool {
|
||||
return queryState == cookieState
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("state") != state {
|
||||
log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state)
|
||||
func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
|
||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get state cookie: %v", err)
|
||||
http.Error(w, "State not found", http.StatusBadRequest)
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
state := cookie.Value
|
||||
|
||||
if !h.validateStateMatch(r.URL.Query().Get("state"), state) {
|
||||
log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state)
|
||||
http.Error(w, "State mismatch", http.StatusBadRequest)
|
||||
return nil, state, false
|
||||
}
|
||||
|
||||
registeredState, ok := h.registeredStates[state]
|
||||
|
||||
if !ok {
|
||||
log.Errorf("State not found in server: %v", state)
|
||||
|
||||
http.Error(w, "State not found in server", http.StatusBadRequest)
|
||||
return nil, state, false
|
||||
}
|
||||
|
||||
return registeredState, state, true
|
||||
|
|
@ -210,13 +213,13 @@ func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
|
|||
caCertPool := x509.NewCertPool()
|
||||
|
||||
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
||||
log.Errorf("OAuth2 Cert Bundle - failed to append certificates: %v", err)
|
||||
log.Errorf("OAuth2 Cert Bundle - failed to append certificates from PEM")
|
||||
}
|
||||
|
||||
return caCertPool
|
||||
}
|
||||
|
||||
func (h *OAuth2Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
log.Infof("OAuth2 Callback received")
|
||||
|
||||
registeredState, state, ok := h.checkOAuthCallbackCookie(w, r)
|
||||
|
|
@ -265,17 +268,7 @@ func (h *OAuth2Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Reque
|
|||
h.registeredStates[state].Username = userinfo.Username
|
||||
h.registeredStates[state].Usergroup = userinfo.Usergroup
|
||||
|
||||
for k, v := range h.registeredStates {
|
||||
log.Debugf("states: %+v %+v", k, v)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"state": state,
|
||||
"username": h.registeredStates[state].Username,
|
||||
}).Info("OAuth2 login successful")
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
w.Write([]byte("OAuth2 login successful."))
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
|
|
@ -351,16 +344,17 @@ func getDataField(data map[string]any, field string) string {
|
|||
return stringVal
|
||||
}
|
||||
|
||||
func (h *OAuth2Handler) parseOAuth2Cookie(r *http.Request) (string, string, string) {
|
||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||
func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
||||
cookie, err := context.Request.Cookie("olivetin-sid-oauth")
|
||||
|
||||
user := &authTypes.AuthenticatedUser{}
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to read OAuth2 cookie: %v", err)
|
||||
return "", "", ""
|
||||
return nil
|
||||
}
|
||||
|
||||
if cookie.Value == "" {
|
||||
return "", "", ""
|
||||
return nil
|
||||
}
|
||||
|
||||
serverState, found := h.registeredStates[cookie.Value]
|
||||
|
|
@ -371,10 +365,13 @@ func (h *OAuth2Handler) parseOAuth2Cookie(r *http.Request) (string, string, stri
|
|||
"provider": "oauth2",
|
||||
}).Warnf("Stale session")
|
||||
|
||||
return "", "", cookie.Value
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("Found OAuth2 state: %+v", serverState)
|
||||
user.Username = serverState.Username
|
||||
user.UsergroupLine = serverState.Usergroup
|
||||
user.Provider = "oauth2"
|
||||
user.SID = cookie.Value
|
||||
|
||||
return serverState.Username, serverState.Usergroup, cookie.Value
|
||||
return user
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package httpservers
|
||||
package otoauth2
|
||||
|
||||
import (
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
|
|
@ -13,11 +13,14 @@ var oauth2ProviderDatabase = map[string]config.OAuth2Provider{
|
|||
WhoamiUrl: "https://api.github.com/user",
|
||||
TokenUrl: endpoints.GitHub.TokenURL,
|
||||
AuthUrl: endpoints.GitHub.AuthURL,
|
||||
Scopes: []string{"profile", "email"},
|
||||
Scopes: []string{"read:user", "user:email"},
|
||||
UsernameField: "login",
|
||||
},
|
||||
"google": {
|
||||
Title: "Google",
|
||||
Name: "google",
|
||||
Icon: "google",
|
||||
UsernameField: "email",
|
||||
WhoamiUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
TokenUrl: endpoints.Google.TokenURL,
|
||||
AuthUrl: endpoints.Google.AuthURL,
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
)
|
||||
|
||||
func UserGuest(cfg *config.Config) *authpublic.AuthenticatedUser {
|
||||
ret := &authpublic.AuthenticatedUser{}
|
||||
ret.Username = "guest"
|
||||
ret.UsergroupLine = "guest"
|
||||
ret.Provider = "system"
|
||||
|
||||
ret.BuildUserAcls(cfg)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func UserFromSystem(cfg *config.Config, username string) *authpublic.AuthenticatedUser {
|
||||
ret := &authpublic.AuthenticatedUser{
|
||||
Username: username,
|
||||
UsergroupLine: "system",
|
||||
Provider: "system",
|
||||
}
|
||||
|
||||
ret.BuildUserAcls(cfg)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
types "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
)
|
||||
|
||||
//gocyclo:ignore
|
||||
func checkUserFromHeaders(context *types.AuthCheckingContext) *types.AuthenticatedUser {
|
||||
u := &types.AuthenticatedUser{}
|
||||
|
||||
if context.Config.AuthHttpHeaderUsername != "" {
|
||||
u.Username = getHeaderKeyOrEmpty(context.Request.Header, context.Config.AuthHttpHeaderUsername)
|
||||
}
|
||||
|
||||
if context.Config.AuthHttpHeaderUserGroup != "" {
|
||||
u.UsergroupLine = getHeaderKeyOrEmpty(context.Request.Header, context.Config.AuthHttpHeaderUserGroup)
|
||||
}
|
||||
|
||||
if prov := getHeaderKeyOrEmpty(context.Request.Header, "provider"); prov != "" {
|
||||
u.Provider = prov
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func getHeaderKeyOrEmpty(headers http.Header, key string) string {
|
||||
values := headers.Values(key)
|
||||
if len(values) > 0 {
|
||||
return values[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sanitize will look for common configuration issues, and fix them. For example,
|
||||
// populating undefined fields - name -> title, etc.
|
||||
func (cfg *Config) Sanitize() {
|
||||
cfg.sanitizeLogLevel()
|
||||
cfg.sanitizeAuthRequireGuestsToLogin()
|
||||
cfg.sanitizeLogHistoryPageSize()
|
||||
|
||||
// log.Infof("cfg %p", cfg)
|
||||
|
||||
|
|
@ -41,12 +44,9 @@ func (action *Action) sanitize(cfg *Config) {
|
|||
for idx := range action.Arguments {
|
||||
action.Arguments[idx].sanitize()
|
||||
}
|
||||
|
||||
sanitizeAuthRequireGuestsToLogin(cfg)
|
||||
sanitizeLogHistoryPageSize(cfg)
|
||||
}
|
||||
|
||||
func sanitizeAuthRequireGuestsToLogin(cfg *Config) {
|
||||
func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
|
||||
if cfg.AuthRequireGuestsToLogin {
|
||||
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ func sanitizeAuthRequireGuestsToLogin(cfg *Config) {
|
|||
}
|
||||
}
|
||||
|
||||
func sanitizeLogHistoryPageSize(cfg *Config) {
|
||||
func (cfg *Config) sanitizeLogHistoryPageSize() {
|
||||
if cfg.LogHistoryPageSize < 10 {
|
||||
log.Warnf("LogsHistoryLimit is too low, setting it to 10")
|
||||
cfg.LogHistoryPageSize = 10
|
||||
|
|
|
|||
|
|
@ -333,12 +333,13 @@ func mangleCheckboxValues(req *ExecutionRequest, arg *config.ActionArgument) {
|
|||
|
||||
log.Infof("Checking checkbox values for argument %s in action %s", arg.Name, req.Binding.Action.Title)
|
||||
|
||||
for i, _ := range arg.Choices {
|
||||
for i, v := range arg.Choices {
|
||||
choice := &arg.Choices[i]
|
||||
|
||||
if req.Arguments[arg.Name] == choice.Title {
|
||||
log.WithFields(log.Fields{
|
||||
"arg": arg.Name,
|
||||
"choice": v,
|
||||
"oldValue": req.Arguments[arg.Name],
|
||||
"newValue": choice.Value,
|
||||
"actionTitle": req.Binding.Action.Title,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package executor
|
|||
|
||||
import (
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/entities"
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -69,7 +71,7 @@ type ExecutionRequest struct {
|
|||
TrackingID string
|
||||
Tags []string
|
||||
Cfg *config.Config
|
||||
AuthenticatedUser *acl.AuthenticatedUser
|
||||
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||
TriggerDepth int
|
||||
|
||||
logEntry *InternalLogEntry
|
||||
|
|
@ -230,11 +232,11 @@ func isValidLogEntryForACL(entry *InternalLogEntry) bool {
|
|||
}
|
||||
|
||||
// isLogEntryAllowedByACL checks if a log entry is allowed to be viewed by the user.
|
||||
func isLogEntryAllowedByACL(cfg *config.Config, user *acl.AuthenticatedUser, entry *InternalLogEntry) bool {
|
||||
func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry) bool {
|
||||
return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
|
||||
}
|
||||
|
||||
func (e *Executor) filterLogsByACL(cfg *config.Config, user *acl.AuthenticatedUser) []*InternalLogEntry {
|
||||
func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser) []*InternalLogEntry {
|
||||
e.logmutex.RLock()
|
||||
defer e.logmutex.RUnlock()
|
||||
|
||||
|
|
@ -280,7 +282,7 @@ func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageC
|
|||
|
||||
// GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and
|
||||
// paginated correctly based on the filtered set.
|
||||
func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *acl.AuthenticatedUser, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
|
||||
func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
|
||||
filtered := e.filterLogsByACL(cfg, user)
|
||||
return paginateFilteredLogs(filtered, startOffset, pageCount)
|
||||
}
|
||||
|
|
@ -323,7 +325,7 @@ func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
|
|||
// ExecRequest processes an ExecutionRequest
|
||||
func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
|
||||
if req.AuthenticatedUser == nil {
|
||||
req.AuthenticatedUser = acl.UserGuest(req.Cfg)
|
||||
req.AuthenticatedUser = auth.UserGuest(req.Cfg)
|
||||
}
|
||||
|
||||
req.executor = e
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
)
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ func TestCreateExecutorAndExec(t *testing.T) {
|
|||
e, cfg := testingExecutor()
|
||||
|
||||
req := ExecutionRequest{
|
||||
AuthenticatedUser: &acl.AuthenticatedUser{Username: "Mr Tickle"},
|
||||
AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "Mr Tickle"},
|
||||
Cfg: cfg,
|
||||
Arguments: map[string]string{
|
||||
"person": "yourself",
|
||||
|
|
@ -273,7 +274,7 @@ func TestMangleInvalidArgumentValues(t *testing.T) {
|
|||
|
||||
req := ExecutionRequest{
|
||||
// Action: a1,
|
||||
AuthenticatedUser: acl.UserFromSystem(cfg, "testuser"),
|
||||
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
|
||||
Cfg: cfg,
|
||||
Arguments: map[string]string{
|
||||
"date": "1990-01-10T12:00", // Invalid format, should be without seconds
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import (
|
|||
"path"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/api"
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
"github.com/OliveTin/OliveTin/internal/auth/otoauth2"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
|
@ -32,13 +34,13 @@ func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// StartSingleHTTPFrontend will create a reverse proxy that proxies the API
|
||||
// and webui internally.
|
||||
func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
||||
func StartFrontendMux(cfg *config.Config, ex *executor.Executor) {
|
||||
log.WithFields(log.Fields{
|
||||
"address": cfg.ListenAddressSingleHTTPFrontend,
|
||||
}).Info("Starting single HTTP frontend")
|
||||
|
||||
go StartPrometheus(cfg)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
apiPath, apiHandler := api.GetNewHandler(ex)
|
||||
|
|
@ -62,10 +64,11 @@ func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
|||
apiHandler.ServeHTTP(w, r)
|
||||
}))
|
||||
|
||||
oauth2handler := NewOAuth2Handler(cfg)
|
||||
oauth2handler := otoauth2.NewOAuth2Handler(cfg)
|
||||
auth.AddAuthChainFunction(oauth2handler.CheckUserFromOAuth2Cookie)
|
||||
|
||||
mux.HandleFunc("/oauth/login", oauth2handler.handleOAuthLogin)
|
||||
mux.HandleFunc("/oauth/callback", oauth2handler.handleOAuthCallback)
|
||||
mux.HandleFunc("/oauth/login", oauth2handler.HandleOAuthLogin)
|
||||
mux.HandleFunc("/oauth/callback", oauth2handler.HandleOAuthCallback)
|
||||
|
||||
mux.HandleFunc("/readyz", handleReadyz)
|
||||
|
||||
|
|
@ -97,5 +100,11 @@ func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
|||
func handleReadyz(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK. Single HTTP Frontend is ready.\n"))
|
||||
_, err := w.Write([]byte("OK. Single HTTP Frontend is ready.\n"))
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Warnf("Failed to write readyz response")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package httpservers
|
||||
|
||||
import (
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
)
|
||||
|
||||
// StartServers will start 3 HTTP servers. The WebUI, the Rest API, and a proxy
|
||||
// for both of them.
|
||||
func StartServers(cfg *config.Config, ex *executor.Executor) {
|
||||
if cfg.Prometheus.Enabled {
|
||||
go StartPrometheus(cfg)
|
||||
}
|
||||
|
||||
StartSingleHTTPFrontend(cfg, ex)
|
||||
}
|
||||
|
|
@ -7,13 +7,25 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func StartPrometheus(cfg *config.Config) {
|
||||
if !cfg.Prometheus.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if !cfg.Prometheus.DefaultGoMetrics {
|
||||
prometheus.Unregister(collectors.NewGoCollector())
|
||||
}
|
||||
|
||||
http.Handle("/", promhttp.Handler())
|
||||
http.ListenAndServe(cfg.ListenAddressPrometheus, nil)
|
||||
err := http.ListenAndServe(cfg.ListenAddressPrometheus, nil)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"address": cfg.ListenAddressPrometheus,
|
||||
"error": err,
|
||||
}).Warnf("Failed to start Prometheus server")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,174 +0,0 @@
|
|||
package httpservers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
// config "github.com/OliveTin/OliveTin/internal/config"
|
||||
// "github.com/golang-jwt/jwt/v4"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
// "time"
|
||||
)
|
||||
|
||||
func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
|
||||
tmpFile, _ := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
|
||||
|
||||
fmt.Println("Created File: " + tmpFile.Name())
|
||||
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
pubKey := &privateKey.PublicKey
|
||||
// https://stackoverflow.com/questions/13555085/save-and-load-crypto-rsa-privatekey-to-and-from-the-disk
|
||||
pkixPubKey, _ := x509.MarshalPKIXPublicKey(pubKey)
|
||||
pubPem := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: pkixPubKey,
|
||||
},
|
||||
)
|
||||
|
||||
if err := os.WriteFile(tmpFile.Name(), pubPem, 0755); err != nil {
|
||||
t.Fatalf("error when dumping pubKey: %s \n", err)
|
||||
}
|
||||
|
||||
return privateKey, tmpFile.Name()
|
||||
}
|
||||
|
||||
func newMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func testJwkValidation(t *testing.T, expire int64, expectCode int) {
|
||||
/*
|
||||
privateKey, publicKeyPath := createKeys(t)
|
||||
|
||||
defer os.Remove(publicKeyPath)
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||
cfg.AuthJwtClaimUsername = "sub"
|
||||
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
||||
cfg.AuthJwtCookieName = "authorization_token"
|
||||
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["nbf"] = time.Now().Unix() - 1000
|
||||
claims["exp"] = time.Now().Unix() + expire
|
||||
claims["sub"] = "test"
|
||||
claims["olivetinGroup"] = "test"
|
||||
*/
|
||||
|
||||
/*
|
||||
tokenStr, _ := token.SignedString(privateKey)
|
||||
|
||||
mux := newMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||
username, usergroup := parseJwtCookie(cfg, r)
|
||||
|
||||
if username == "" {
|
||||
w.WriteHeader(403)
|
||||
}
|
||||
|
||||
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
|
||||
})
|
||||
|
||||
srv := setupTestingServer(mux, t)
|
||||
|
||||
req, client := newReq("")
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "authorization_token",
|
||||
Value: tokenStr,
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
res, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Client err: %+v", err)
|
||||
} else {
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, expectCode, res.StatusCode)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
fmt.Println(string(body))
|
||||
}
|
||||
|
||||
err = srv.Shutdown(context.TODO())
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Server shutdown error: %+v", err)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func TestJWTSignatureVerificationSucceeds(t *testing.T) {
|
||||
testJwkValidation(t, 1000, 200)
|
||||
}
|
||||
|
||||
func TestJWTSignatureVerificationFails(t *testing.T) {
|
||||
testJwkValidation(t, -500, 403)
|
||||
}
|
||||
|
||||
func TestJWTHeader(t *testing.T) {
|
||||
/*
|
||||
privateKey, publicKeyPath := createKeys(t)
|
||||
|
||||
defer os.Remove(publicKeyPath)
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||
cfg.AuthJwtClaimUsername = "sub"
|
||||
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
||||
cfg.AuthJwtHeader = "Authorization"
|
||||
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["nbf"] = time.Now().Unix() - 1000
|
||||
claims["exp"] = time.Now().Unix() + 2000
|
||||
claims["sub"] = "test"
|
||||
claims["olivetinGroup"] = []string{"test", "test2"}
|
||||
*/
|
||||
|
||||
/*
|
||||
tokenStr, _ := token.SignedString(privateKey)
|
||||
|
||||
mux := newMux()
|
||||
mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||
username, usergroup := parseJwtHeader(cfg, r)
|
||||
|
||||
if username == "" {
|
||||
w.WriteHeader(403)
|
||||
}
|
||||
|
||||
assert.Equal(t, "test", username)
|
||||
assert.Equal(t, "test test2", usergroup)
|
||||
|
||||
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
|
||||
})
|
||||
|
||||
srv := setupTestingServer(mux, t)
|
||||
|
||||
req, client := newReq("")
|
||||
req.Header.Set("Authorization", "Bearer "+tokenStr)
|
||||
|
||||
res, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Client err: %+v", err)
|
||||
} else {
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, 200, res.StatusCode)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
fmt.Println(string(body))
|
||||
}
|
||||
|
||||
srv.Shutdown(context.TODO())
|
||||
*/
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package httpservers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func parseLocalUserCookie(cfg *config.Config, req *http.Request) (string, string, string) {
|
||||
cookie, err := req.Cookie("olivetin-sid-local")
|
||||
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
cookieValue := cookie.Value
|
||||
|
||||
session := auth.GetUserSession("local", cookieValue)
|
||||
if session == nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
user := cfg.FindUserByUsername(session.Username)
|
||||
if user == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"username": session.Username,
|
||||
}).Warnf("User not found in config")
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
return user.Username, user.Usergroup, cookie.Value
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package httpservers
|
||||
|
||||
import ()
|
||||
|
|
@ -93,22 +93,42 @@ func (s *webUIServer) setupCustomWebuiDir() {
|
|||
}
|
||||
}
|
||||
|
||||
func shouldReloadThemeCss() bool {
|
||||
return !customThemeCssRead
|
||||
}
|
||||
|
||||
func loadThemeCssFromFile(filename string) []byte {
|
||||
_, err := os.Stat(filename)
|
||||
if err == nil {
|
||||
css, err := os.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Tracef("Theme CSS file not read: %s", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
return css
|
||||
}
|
||||
|
||||
log.Tracef("Theme CSS file not found: %s", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *webUIServer) generateThemeCss(w http.ResponseWriter, r *http.Request) {
|
||||
themeCssFilename := path.Join(s.findCustomWebuiDir(), "themes", s.cfg.ThemeName, "theme.css")
|
||||
|
||||
if !customThemeCssRead || s.cfg.ThemeCacheDisabled {
|
||||
if shouldReloadThemeCss() || s.cfg.ThemeCacheDisabled {
|
||||
customThemeCssRead = true
|
||||
|
||||
if _, err := os.Stat(themeCssFilename); err == nil {
|
||||
customThemeCss, _ = os.ReadFile(themeCssFilename)
|
||||
} else {
|
||||
log.Debugf("Theme CSS not read: %v", err)
|
||||
customThemeCss = []byte("/* not found */")
|
||||
}
|
||||
customThemeCss = loadThemeCssFromFile(themeCssFilename)
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/css")
|
||||
w.Write(customThemeCss)
|
||||
_, err := w.Write(customThemeCss)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Warnf("Failed to write theme CSS")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *webUIServer) handleCustomWebui() http.Handler {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ package oncalendarfile
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/OliveTin/OliveTin/internal/acl"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Schedule(cfg *config.Config, ex *executor.Executor) {
|
||||
|
|
@ -105,7 +106,7 @@ func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *exec
|
|||
Binding: ex.FindBindingWithNoEntity(action),
|
||||
Cfg: cfg,
|
||||
Tags: []string{},
|
||||
AuthenticatedUser: acl.UserFromSystem(cfg, "calendar"),
|
||||
AuthenticatedUser: auth.UserFromSystem(cfg, "calendar"),
|
||||
}
|
||||
|
||||
ex.ExecRequest(req)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package oncron
|
||||
|
||||
import (
|
||||
"github.com/OliveTin/OliveTin/internal/acl"
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
|
@ -37,7 +37,7 @@ func scheduleAction(cfg *config.Config, scheduler *cron.Cron, cronline string, e
|
|||
Binding: ex.FindBindingWithNoEntity(action),
|
||||
Cfg: cfg,
|
||||
Tags: []string{},
|
||||
AuthenticatedUser: acl.UserFromSystem(cfg, "cron"),
|
||||
AuthenticatedUser: auth.UserFromSystem(cfg, "cron"),
|
||||
}
|
||||
|
||||
ex.ExecRequest(req)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/acl"
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||
|
|
@ -54,7 +54,7 @@ func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Execut
|
|||
Cfg: cfg,
|
||||
Tags: []string{},
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromSystem(cfg, "fileindir"),
|
||||
AuthenticatedUser: auth.UserFromSystem(cfg, "fileindir"),
|
||||
}
|
||||
|
||||
ex.ExecRequest(req)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
package onstartup
|
||||
|
||||
import (
|
||||
"github.com/OliveTin/OliveTin/internal/acl"
|
||||
"github.com/OliveTin/OliveTin/internal/auth"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Execute(cfg *config.Config, ex *executor.Executor) {
|
||||
user := acl.UserFromSystem(cfg, "startup")
|
||||
user := auth.UserFromSystem(cfg, "startup")
|
||||
|
||||
for _, action := range cfg.Actions {
|
||||
if action.ExecOnStartup {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,13 @@ func getConfigPath(directory string) string {
|
|||
|
||||
func initConfig(configDir string) {
|
||||
k := koanf.New(".")
|
||||
k.Load(env.Provider(".", ".", nil), nil)
|
||||
err := k.Load(env.Provider(".", ".", nil), nil)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Fatalf("Error loading environment variables")
|
||||
}
|
||||
|
||||
directories := []string{
|
||||
configDir,
|
||||
|
|
@ -180,13 +186,26 @@ func initConfig(configDir string) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
f.Watch(func(evt interface{}, err error) {
|
||||
err := f.Watch(func(evt interface{}, err error) {
|
||||
log.Infof("config file changed: %v", evt)
|
||||
|
||||
k.Load(f, yaml.Parser())
|
||||
errLoad := k.Load(f, yaml.Parser())
|
||||
|
||||
if errLoad != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": errLoad,
|
||||
}).Fatalf("Error loading config file")
|
||||
}
|
||||
|
||||
config.AppendSource(cfg, k, configPath)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Fatalf("Error watching config file")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -251,5 +270,5 @@ func main() {
|
|||
// Load persistent sessions from disk
|
||||
auth.LoadUserSessions(cfg)
|
||||
|
||||
httpservers.StartServers(cfg, executor)
|
||||
httpservers.StartFrontendMux(cfg, executor)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue