Compare commits

...

160 Commits

Author SHA1 Message Date
James Read
709d6ac2ad feature: Allow rendering output as HTML (#544)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-03-28 23:48:26 +00:00
jamesread
8d4e335dda fix: Invalid path to systemd file
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-03-23 01:02:22 +00:00
jamesread
44a9de080c docs: Windows was not bold. 2025-03-23 00:28:07 +00:00
jamesread
9bb17badad docs: Links and make targets 2025-03-23 00:26:52 +00:00
James Read
ff31abe66c refactor: Project directories (#541)
Some checks failed
Build Snapshot / build-snapshot (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
Buf CI / buf (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-03-22 01:06:59 +00:00
James Read
b4c886d5d3 refactor: systemd file does not need to be in root dir (#538) 2025-03-22 01:06:22 +00:00
dependabot[bot]
e0bc1d86f6 build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#540)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-22 00:42:53 +00:00
James Read
270f20ec75 refactor: Moved proto files into own folder (#537)
Some checks failed
Buf CI / buf (push) Waiting to run
Build Snapshot / build-snapshot (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-03-21 02:41:41 +00:00
James Read
486253b253 feature: Docker ce (#536) 2025-03-21 00:15:44 +00:00
James Read
6b9c6c8b9c refactor: Switch to conventional scripts, rather than custom script (#535) 2025-03-20 23:04:36 +00:00
dependabot[bot]
1275934ac1 build(deps): bump axios from 1.7.4 to 1.8.4 in /integration-tests (#534)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Read <contact@jread.com>
2025-03-20 23:00:03 +00:00
jamesread
bbc6095e36 doc: social banner 2025-03-20 22:44:25 +00:00
jamesread
7906f2d363 refactor: dep update 2025-03-20 21:55:17 +00:00
jamesread
d45bd887c2 security: dep update 2025-03-20 21:44:06 +00:00
James Read
7788f58aac feature: persist local sessions across restart (#522)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-21 22:26:15 +00:00
James Read
f3bc82311d feature: Allow arbitary HTML on argument form (useful for descriptions) (#519) 2025-02-21 17:57:02 +00:00
James Read
cae5d296ca bugfix: Fixed broken log message for Execution finished on websocket (#520) 2025-02-21 17:56:37 +00:00
James Read
5cd5bd2a25 bugfix: Default config now uses "triggers" instead of "trigger", and shows HTML (#521) 2025-02-21 17:53:19 +00:00
James Read
6550223ee4 fmt: code style (#518)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-02-19 23:18:02 +00:00
James Read
39368d511a feature: Limit log history to prevent browser lag and grpc encode failures (#507)
* feature: Limit log history to prevent browser lag and grpc encode failures

* cicd: Simplify if/else chain to switch

* cicd: Lint failure in JS because of a trailing space

* cicd: Coderabbit fixes on PR

* cicd: Move config sanitize into correct place

* cicd: Handle negative startOffset, negative endOffset and empty logs

* Update internal/executor/executor.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* cicd: Fix getLogs segfault

* cicd: Fix code rabbit nitpicks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-19 23:17:06 +00:00
James Read
0a4bcc6423 cicd: Update .goreleaser.yml file to Feb 2025 syntax (#517) 2025-02-19 16:57:32 +00:00
jamesread
56ab1cec8f cicd: Textarea resizing [skip ci] 2025-02-18 17:49:54 -08:00
jamesread
12f87ca6e1 feature: Add support for raw textboxes #490 2025-02-18 17:49:54 -08:00
James Read
2fc7c23416 feature!: Trigger changed Triggers, allowing multiple actions (#515)
* feature!: Trigger changed Triggers, allowing multiple actions

* bugfix: Warning message on trigger loops, prevented NPE

* cicd: Fix cyclo complexity with triggers
2025-02-19 00:00:33 +00:00
James Read
2cf538bab1 bugfix: Systemd unit now waits for network and filesystem #495 (#514)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2025-02-18 23:17:56 +00:00
dependabot[bot]
906b6c5783 build(deps): bump serialize-javascript and mocha in /integration-tests (#513)
Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) to 6.0.2 and updates ancestor dependency [mocha](https://github.com/mochajs/mocha). These dependencies need to be updated together.


Updates `serialize-javascript` from 6.0.0 to 6.0.2
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v6.0.0...v6.0.2)

Updates `mocha` from 10.4.0 to 10.8.2
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v10.4.0...v10.8.2)

---
updated-dependencies:
- dependency-name: serialize-javascript
  dependency-type: indirect
- dependency-name: mocha
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 17:14:25 +00:00
jamesread
6d43ebef44 docs: Change maturity label
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-23 14:58:26 +00:00
James Read
c04203e671 Update README.md (#508)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2025-01-22 14:07:21 +00:00
dependabot[bot]
bf93707787 build(deps): bump github.com/MicahParks/jwkset from 0.5.17 to 0.7.0 (#506)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Bumps [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset) from 0.5.17 to 0.7.0.
- [Release notes](https://github.com/MicahParks/jwkset/releases)
- [Commits](https://github.com/MicahParks/jwkset/compare/v0.5.17...v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/MicahParks/jwkset
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-12 21:39:18 +00:00
Bjorn Lammers
f0d70f0c15 Migrate dashboard-icons to homarr-labs
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-06 22:21:35 +00:00
jamesread
17d9e29f19 cicd: Add responsibility labels to all new issues
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2024-12-30 02:05:05 +00:00
jamesread
be7168d9c5 cicd: Issue responsibility bugs 2024-12-30 01:59:27 +00:00
jamesread
d36b23832c cicd: Issue responsibility bugs 2024-12-30 01:54:36 +00:00
jamesread
b6429e9bc7 cicd: Add issue responsibility labels 2024-12-30 01:45:51 +00:00
jamesread
7ddc112b2c doc: CONTRIBUTING - add more guidelines
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2024-12-23 17:14:55 +00:00
jamesread
10a473ca1c security: Big dependency update
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-12-22 01:59:43 +00:00
jamesread
c7207d1ee6 security: Big dependency update 2024-12-22 01:58:19 +00:00
jamesread
c585762ba8 cicd: Standardize the automated code generation 2024-12-22 01:48:21 +00:00
James Read
476838d59a bugfix: Local users now work with a single usergroup (#486)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-11-24 21:22:24 +00:00
James Read
f0b1cefb72 feature: OAuth2 early support for CertBundles (#474) and Usergroups (#477) (#485)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2024-11-24 10:00:25 +00:00
James Read
b4a555e3da bugfix: #481 StartActionAndWait now allows arguments. (#483) 2024-11-24 06:56:27 +00:00
James Read
79a4119351 security: Update cross-spawn vulnerable dependency (#482) 2024-11-24 06:56:16 +00:00
jamesread
851d0dac84 doc: Fix #478 - easy.cfg typo [skip ci] 2024-11-23 16:31:23 -06:00
James Read
655a7f205d feature: Improved auth log messages (#476)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-11-14 22:00:08 +00:00
James Read
13e48dded2 feature: Better guest handling (#472)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
* feature: Better guest handling

* bugfix: Better handling of requiring guests to login
2024-11-09 23:27:09 +00:00
dependabot[bot]
344df3739d build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#473)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-09 00:13:32 +00:00
jamesread
0d981773b3 fmt: gofmt 2024-11-08 23:54:32 +00:00
jamesread
8d4ddd36cd cicd: Goreleaser tikeout
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-11-02 07:35:36 +00:00
James Read
209234c09f bugfix: Still hide an empty actions section if a section is currently selected (#469) 2024-11-02 01:32:02 +00:00
James Read
9bcb2d80dc bugfix: Logs table only updating on start, not finish (#467)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2024-11-02 00:47:13 +00:00
jamesread
de504f5f80 cicd: Lint issue 2024-11-01 23:23:02 +00:00
jamesread
8fe91101db feature: Log search box that is less visually offensive 2024-11-01 23:23:02 +00:00
James Read
8717997b0e feature: Hovering over the execution dialog title shows the action ID and execution ID (#468) 2024-11-01 22:49:09 +00:00
James Read
1bfb7c4b28 feature: Logs table creates an entry as soon as the execution is started (#464) 2024-11-01 21:23:22 +00:00
James Read
964dc7b48a feature: Rerun button on execution dialog, easily rerun actions! (#465) 2024-11-01 20:49:45 +00:00
James Read
ceb0a78180 bugfix: Logout cookies causing a conflict (#461) 2024-11-01 20:36:38 +00:00
James Read
9117163316 bugfix: Login form was not displayed if only OAuth2 was configured (#457)
* bugfix: Login form was not displayed if only OAuth2 was configured

* bugfix: npe in AuthOAuth2Provider

* test: Try and fix flakey tests
2024-10-27 08:56:28 +00:00
James Read
d3ad811ac5 bugfix: Removed hardcoded dependency on GitHub in OAuth2 flow (#458) 2024-10-26 22:30:08 +00:00
James Read
ee26fe6b50 ci github int tests settle (#459)
* ci: Allow more time for GitHub tests to settle

* ci: Run build-snapshot on any push, but dont dupe with PR
2024-10-26 22:10:33 +00:00
James Read
2950b59514 cicd: Devskim actions upgrade (#456) 2024-10-25 13:44:08 +00:00
James Read
be5ddda020 feature: freebsd support (#454) 2024-10-23 12:52:25 +00:00
James Read
16b07283b0 feature: Add riscv build! (#453) 2024-10-23 10:54:43 +00:00
jamesread
d7814ff6df bugfix: If a button is disabled in the API, render it disabled [skip ci] 2024-10-20 23:03:10 +01:00
James Read
be9b2a7c78 bugfix: Cookie expiry for OAuth2 & Local set to 1 year, not session (#451) 2024-10-20 21:58:12 +00:00
James Read
80e5b5b0c1 feature: Cleanup auth code (#444) 2024-10-19 19:33:27 +00:00
James Read
0283b51eca feature: Login/Logout links (#443)
* feature: Login/Logout links

* cicd: codestyle
2024-10-19 07:49:41 +00:00
jamesread
1af2e92132 feature: Local user login! 2024-10-18 01:20:04 +01:00
James Read
71ad5d2e3a feature: local auth! (#441)
* feature: local auth!

* cicd: contentLogin -> content-login

* bugfix: CSS codestyle
2024-10-17 13:23:01 +00:00
James Read
32b5fee108 bugfix: Fixed crash when requesting execution status that could not be found (NPE) (#440) 2024-10-15 10:48:55 +00:00
James Read
de81ec00fd feature: mix of bits (#439)
* feature: mix of bits

* bugfix: codestyle
2024-10-15 07:47:51 +00:00
Simon Dahlbacka
7cc07158ab bugfix: Turns out this should now be 403 instead (#438) 2024-10-14 13:53:23 +00:00
jamesread
3b8976fd51 feature: Login screen 2024-10-14 09:26:33 +01:00
jamesread
fb7e650267 feature: OAuth2 Redirect URL 2024-10-14 09:25:21 +01:00
jamesread
6a7187fb5b feature: OAuth2! :-D 2024-10-14 09:25:21 +01:00
James Read
b31cdf15a2 feature: Email argument type (#433)
* feature: Email argument type

* bugfix: Fix error if additional links is null
2024-10-13 18:40:16 +00:00
jamesread
6e0e0e8133 cicd: cleanup console log 2024-10-13 01:10:47 +01:00
jamesread
706441799f feature: Custom navigation links in the nav (sidebar/topbar) 2024-10-13 00:35:47 +01:00
jamesread
0b66bc7bbd cicd: Labels on issue templates [skip ci] 2024-10-13 00:35:28 +01:00
jamesread
86e876a9c9 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-10-12 23:18:29 +01:00
jamesread
8b33061bbb cicd: Remove unused Jenkinsfile 2024-10-12 23:15:06 +01:00
James Read
411f1bff1c feature: Style the currently selected section in nav, and few minor style improvements (#429)
* feature: Style the currently selected section in nav, and few minor style improvements

* bugfix: No unit on 0 length
2024-10-12 21:45:26 +00:00
James Read
eee4c4e404 bugfix: Auth in the wrong place (#430) 2024-10-12 21:14:09 +00:00
James Read
a187c8c8a0 feature: Slightly nicer looking logs and diagnostics pages (#427) 2024-10-11 17:40:40 +00:00
Simon Dahlbacka
35dca50863 Make it possible to redirect to a login url unless authenticated (#347)
* feature: Try to navigate to UrlOnUnauthenticated if set and not authenticated

* refactor: use better naming

* feature: default to allow guest
2024-10-11 17:36:31 +00:00
Simon Dahlbacka
00856f15a7 feature: pass ot_ args to shellAfterCompleted (#420)
* feature: pass ot_ args to shellAfterCompleted

* refactor: Implement this the right way

---------

Co-authored-by: James Read <contact@jread.com>
2024-10-11 14:59:02 +00:00
James Read
d1c67a9dd8 depbump: js micromatch 4.0.8 (#426) 2024-10-10 23:38:35 +00:00
James Read
6c289506c2 bugfix: Fixed unusual issues that slipped through with shellAfterCompleted (#425)
* bugfix: Fixed unusual issues that slipped through with shellAfterCompleted #419 #420

* cicd: Fix broken style lint
2024-10-10 23:15:03 +00:00
James Read
5d82ae7680 feature: Log username with each request #422 (#424) 2024-10-10 20:50:47 +00:00
James Read
9737886839 feature: Better websocket error message (#417) 2024-10-02 00:19:49 +00:00
James Read
1d9502d800 Feature better 404s (#416)
* feature: CSS on actions

* feature: Less crashes when things are not found
2024-10-01 23:21:37 +00:00
James Read
c0a18f82a5 feature: Link to log outputs (#415) 2024-10-01 23:17:09 +00:00
jamesread
9723a38ca1 bugfix: Crash on trying to start a non-existant action from the API 2024-10-01 21:19:17 +01:00
James Read
8258a758d2 bugfix: Dashboards with multiple spaces in the name could not be navigated properly (#408) 2024-09-16 22:36:40 +00:00
James Read
044613ae9a bugfix: Null action map, causing crash on startup actions (#407) 2024-09-11 16:19:52 +00:00
James Read
76c9171356 feature: Add OT_ vars to each execution (#406) 2024-09-10 22:52:46 +00:00
James Read
f8c330aae3 feature: Better feedback on shellAfterCommand, and mark OliveTin specific output with "OliveTin::" in logs (#403) 2024-09-02 22:59:19 +00:00
jamesread
d4bd7dd586 doc: shellAfterCompleted should use output, not stdout [skip ci] 2024-09-02 21:14:34 +01:00
Mitch Brown
fa20f6a63a Fixed typo 2024-09-01 00:22:45 +01:00
James Read
b9ce695616 feature: Nicer default colors (catpuccin/frappe (#395) 2024-08-30 20:48:17 +00:00
James Read
3f1dbf1130 bugfix: System users can now be used in ACLs (#396) 2024-08-30 20:48:07 +00:00
James Read
6413e51cf5 feature: Exec permission denied now marks the action as "blocked" and adds a log message. Timeouts also add a documentation link to the command output. Tags always set. (#398) 2024-08-30 20:36:13 +00:00
James Read
defcf6d26e fmt: Rebuilt ACL code to have less boilerplate (#397) 2024-08-30 20:30:44 +00:00
James Read
2671983c43 bugfix: Execution dialog duration no longer has issues displaying actions that were never started. (#394) 2024-08-30 20:28:27 +00:00
James Read
dc9653307b feature: Logs, column for tag, and left align status. Stop nav flash (#393) 2024-08-30 20:28:12 +00:00
jamesread
eb91eb33d5 bugfix: Log for http request mangling changed to DEBUG level 2024-08-30 21:27:56 +01:00
James Read
ddb803d9b5 feature: Sidebar hiding now pure CSS & theme reloading for devs (#391) 2024-08-25 23:47:43 +00:00
dependabot[bot]
c9095b4d67 build(deps): bump axios from 1.6.8 to 1.7.4 in /integration-tests (#387)
Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.8...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 22:14:35 +00:00
James Read
40158eda71 bugfix: Race condition on executor logs (#385) 2024-08-14 22:06:50 +00:00
dependabot[bot]
bbbbfceeb3 build(deps): bump github.com/docker/docker (#386)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.1.4+incompatible to 26.1.5+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.1.4...v26.1.5)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 22:06:06 +00:00
James Read
9ca1940834 feature: change all navigation to path based (#384)
* feature: change all navigation to path based

* bugfix: load default dashboard if actions is empty
2024-08-14 11:35:10 +00:00
James Read
37160a91f3 feature: take full page and display footer at bottom pf window (#383) 2024-08-14 06:56:30 +00:00
James Read
274d036f74 fmt: Cleanup terminal code (#381)
* fmt: Clean up OutputTerminal code

* fmt: Clean up OutputTerminal code
2024-08-09 19:04:57 +00:00
James Read
1fe0e49adb bugfix: #337 - Entity overwriting (#382) 2024-08-09 19:04:50 +00:00
James Read
7a7a07d9ad feature: Minor change to action timeout message to be more consistent with other logs (#380) 2024-08-09 14:53:02 +00:00
James Read
5b5fca0837 feature: enableCustomJs toggle to allow loading ./custom-webui/custom.js (#379) 2024-08-09 09:21:50 +00:00
James Read
fab0264d9b feature: Cleanup execution dialog, and log display status. (#378)
* feature: Cleanup execution dialog, and log display status.

* fmt: Remove empty block

* cicd: Use ActionStatusDisplay in test

* bugfix: missed promise

* bugfix: missed promise

* bugfix: Didnt return getText on ActionStatusDisplay
2024-08-08 20:46:46 +00:00
jamesread
8d839ee6ce bugfix: fsnotify event logs moved to DEBUG level, because info was spammy 2024-08-07 00:22:49 +01:00
jamesread
90efbf3159 fmt: #368 2024-08-07 00:21:02 +01:00
jamesread
17dd1b4158 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-08-07 00:17:42 +01:00
dependabot[bot]
e5f6d8ff50 build(deps): bump github.com/docker/docker (#372)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.0.2+incompatible to 26.1.4+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.0.2...v26.1.4)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 16:02:13 +00:00
jamesread
e183910b88 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-08-06 16:35:10 +01:00
James Read
8cd5b9fb46 feature: Trace log message on ExecRequest for debugging (#375) 2024-08-06 13:46:49 +00:00
wushuzh
6958445f83 Use windows compatible cmds and wait more time (#374)
* bugfix: change action request cnt type as Counter

* bugfix: fix type of act req count and typo

* bugfix: change to correct type of act req in TC

* bugfix: use Windows compatible cmds for test

* bugfix: wait more for slow test env

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-08-06 13:43:16 +00:00
James Read
ef91294d5e cicd: Make grpc to fix codeql (#373) 2024-07-30 13:03:56 +00:00
jamesread
a36f286f8a cicd: Make grpc to fix codeql 2024-07-30 13:58:12 +01:00
wushuzh
d9e921950f bugfix: fix type of the prometheus metric act req as count (#370)
* bugfix: change action request cnt type as Counter

* bugfix: fix type of act req count and typo

* bugfix: change to correct type of act req in TC

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-07-26 08:41:42 +00:00
wushuzh
ffcd19e748 bugfix: use cross-platform module filepath to set usedConfigDir (#369)
* bugfix: use filepath to set correct configDir in Windows

* test: improve unittest rep folder creation

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-07-22 09:33:56 +00:00
jamesread
d0eb132b95 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-07-18 15:11:19 +01:00
jamesread
1cf971c092 feature: Better format for server dashboard 2024-07-18 15:11:06 +01:00
James Read
49f745be68 fmt: Set for setdir (#367) 2024-07-18 12:08:36 +00:00
wushuzh
b50824a705 feature: support basic dev tasks in Windows (#364)
* feature: support basic dev tasks in Windows

* feature: support front-end targets in Windows

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-07-17 12:51:08 +00:00
James Read
69d1cc75a7 feature: css classes on displays (#363) 2024-07-16 10:01:31 +00:00
jamesread
a54ea505c9 bugfix: Allow . in enxtended variable paths 2024-07-15 22:20:49 +01:00
James Read
a1563b72ae Feature argument grid layout (#360)
* feature: Argument form now uses a grid layout, making the input boxes take up available room

* feature: Argument form now uses a grid layout, making the input boxes take up available room

* feature: Argument form now uses a grid layout, making the input boxes take up available room

* feature: Argument form now uses a grid layout, making the input boxes take up available room
2024-07-15 16:17:08 +00:00
James Read
31d7168aac feature: #350 Environment variable PORT can be used to override the default 1337, and internal services will calculate their port based on that (#358) 2024-07-15 14:28:53 +00:00
James Read
09016e1d5f feature: Entitiy data now serializes non-strings (#357) 2024-07-15 12:10:13 +00:00
James Read
652882350c cicd: Update login-action (#356) 2024-07-15 12:09:35 +00:00
James Read
20c4423799 feature: Argument labels will end with a :, unless they end with ?, : or . already (#355) 2024-07-15 08:24:54 +00:00
jamesread
bb90a5da92 feature: Use SVG icon for window maximize #343 2024-07-13 20:46:06 +01:00
jamesread
3ca3a2dd3c Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-07-13 20:43:21 +01:00
jamesread
510c48e1af bugfix: Restore default theme 2024-07-13 20:43:04 +01:00
James Read
3ac809c234 feature: Container images upgraded to Fedora 40. (#354) 2024-07-13 18:36:10 +00:00
James Read
9dd33bc3f9 bugfix: Empty environment variable names causing exec failures on Windows, thanks @sirjmann92 ! (#353) 2024-07-12 16:22:03 +00:00
James Read
897cc0e034 depbump: Upgrade Go to 1.21 (#352) 2024-07-11 22:33:36 +00:00
James Read
482ef0e5e8 feature: Helper scripts for ssh, themes, etc (#349)
* feature: Helper scripts for ssh, themes, etc

* cicd: Update goreleaser to support v2 .goreleaser.yml files
2024-07-07 22:10:33 +00:00
dependabot[bot]
e0678fc0a9 build(deps): bump github.com/rs/cors from 1.10.1 to 1.11.0 (#348)
Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.10.1 to 1.11.0.
- [Commits](https://github.com/rs/cors/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/rs/cors
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Read <contact@jread.com>
2024-07-07 07:01:33 +00:00
jamesread
9cb5574b99 feature: easy-ssh-docs 2024-07-07 01:06:43 +01:00
jamesread
c18b91f684 feature: Easy SSH 2024-07-07 00:55:22 +01:00
James Read
6622a6ded4 bugfix: Kill the process group, not just the parent process (#328), , thanks @ioqy, @vvrein (#346)
* bugfix: Kill the process group, not just the parent process (#328), thanks @ioqy, @vvrein

* bugfix: Kill the process group, not just the parent process (#328), thanks @ioqy, @vvrein

* bugfix: Kill the process group, not just the parent process (#328), thanks @ioqy, @vvrein
2024-07-04 00:12:28 +00:00
James Read
fb6aaa52c7 feature: New diagnostics page, showing SSH keys found. (#344) 2024-07-03 23:34:07 +00:00
James Read
a1adc2a85d bugfix: Output width in chrome (#345) 2024-07-03 23:33:58 +00:00
dependabot[bot]
3d9cb621dd build(deps-dev): bump braces from 3.0.2 to 3.0.3 in /integration-tests
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-04 00:34:34 +01:00
James Read
943b4c75aa feature: Password type (no validation) (#340) 2024-07-01 08:31:53 +00:00
James Read
fb972fae55 bugfix: Handle update failed update checks silently (#342) 2024-06-30 22:05:04 +00:00
dependabot[bot]
eb4f28dfda build(deps-dev): bump ws from 8.16.0 to 8.17.1 in /integration-tests (#335)
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 05:25:03 +00:00
James Read
e36fedf4b2 Check for updates default disabled (#333)
* feature: Checking for updates is now disabled by default (#93)

* feature: Default config has checkForUpdates: false (it defaults to false anyway) to make people aware of the update checking feature
2024-06-04 18:37:59 +00:00
James Read
362a97c59e feature: No more tracking in update checking! :-) (#93) (#332) 2024-06-04 12:21:11 +00:00
James Read
c82beb61a9 feature: Vastly improved logs and tests for killing actions (#330)
* bugfix: Sleep testing

* feature: Vastly improved testing for killing actions (#328)

* cicd: Add find-flakey-tests target

* cicd: Better debugging for domStatus

* cicd: Debug flakey test

* cicd: Debug flakey test

* cicd: Is cors messing with killaction?

* cicd: Slip broken test in CI

* cicd: Skip broken test in CI

* cicd: Skip broken test in CI
2024-06-03 16:19:15 +00:00
James Read
00a8a0bf69 fmt: Center argument-wrapper #327, point 5 (#331) 2024-06-02 22:09:24 +00:00
James Read
ffc17dd73b bugfix: Terminal width fitting (#329)
* bugfix: Terminal width fitting

* bugfix: Terminal width fitting

* fmt: css codestyle issue presumably from pkg upgrade
2024-06-02 11:44:23 +00:00
130 changed files with 7381 additions and 7407 deletions

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env python3
import sys
commitmsg = ""
with open('.git/COMMIT_EDITMSG', mode='r') as f:
commitmsg = f.readline().strip()
print("Commit message is: " + commitmsg)
ALLOWED_COMMIT_TYPES = [
"cicd",
"test",
"refactor",
"depbump",
"typo",
"fmt",
"doc",
"bugfix",
"security",
"feature",
]
for allowedType in ALLOWED_COMMIT_TYPES:
if commitmsg.startswith(allowedType + ":"):
print("Allowing commit type: ", allowedType)
sys.exit(0)
print("Commit message should start with commit type. One of: ", ", ".join(ALLOWED_COMMIT_TYPES))
sys.exit(1)

View File

@@ -2,7 +2,9 @@
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
labels:
- "type: bug"
- "waiting-on-developer"
assignees: ''
---

View File

@@ -2,7 +2,9 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
labels:
- "type: feature-request"
- "waiting-on-developer"
assignees: ''
---

View File

@@ -2,7 +2,9 @@
name: Support request
about: Need some help? Got an error message?
title: ""
labels: support
labels:
- "type: support"
- "waiting-on-developer"
assignees: ''
---

View File

@@ -14,9 +14,10 @@ Helpful information to understand the project can be found here: [CONTRIBUTING](
Please put a X in the boxes as evidence of reading through the checklist.
- [ ] I have forked the project, and raised this PR on a feature branch.
- [ ] `make githooks` has been run, and my git commit message was accepted by the git hook.
- [ ] `make daemon-compile` runs without any issues.
- [ ] `make daemon-codestyle` runs without any issues.
- [ ] `make daemon-unittests` runs without any issues.
- [ ] `make webui-codestyle` runs without any issues.
- [ ] I ran the `pre-commit` hooks, and my commit message was validated.
- [ ] `make -wC service compile` runs without any issues.
- [ ] `make -wC service codestyle` runs without any issues.
- [ ] `make -wC service unittests` runs without any issues.
- [ ] `make -wC webui codestyle` runs without any issues.
- [ ] `make -w it` runs without any issues.
- [ ] I understand and accept the [AGPL-3.0 license](LICENSE) and [code of conduct](CODE_OF_CONDUCT.md), and my contributions fall under these.

38
.github/workflows/build-buf.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Buf CI
on:
push:
paths:
- 'proto/**'
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
pull-requests: write
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'service/go.mod'
cache: true
- name: grpc
run: make -w grpc
- name: make service
run: make -w service
- uses: bufbuild/buf-action@v1.1.0
with:
token: ${{ secrets.BUF_TOKEN }}
# Change setup_only to true if you only want to set up the Action and not execute other commands.
# Otherwise, you can delete this line--the default is false.
setup_only: false
# Optional GitHub token for API requests. Ensures requests aren't rate limited.
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,8 +4,6 @@ name: "Build Snapshot"
on:
push:
workflow_dispatch:
pull_request:
branches: [main]
jobs:
build-snapshot:
@@ -33,26 +31,29 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '^1.18.0'
go-version-file: 'service/go.mod'
cache: true
- name: Print go version
run: go version
- name: grpc
run: make -w grpc
- name: make daemon
run: make -w daemon-compile-x64-lin
- name: make service
run: make -w service
- name: make webui
run: make -w webui-dist
- name: unit tests
run: make -w daemon-unittests
run: make -w service-unittests
- name: integration tests
run: cd integration-tests && make -w
- name: goreleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
@@ -70,6 +71,7 @@ jobs:
- name: Archive integration tests
uses: actions/upload-artifact@v4.3.1
if: always()
with:
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
path: |

View File

@@ -31,17 +31,20 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '^1.18.0'
go-version-file: 'service/go.mod'
cache: true
- name: Print go version
run: go version
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_KEY }}
- name: Login to ghcr
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -54,11 +57,11 @@ jobs:
run: make -w webui-dist
- name: goreleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release --clean --parallelism 1
args: release --clean --timeout 60m
env:
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}

View File

@@ -44,6 +44,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'service/go.mod'
cache: true
- name: grpc
run: make -w grpc
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3

View File

@@ -21,14 +21,17 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '^1.18.0'
go-version-file: 'service/go.mod'
cache: true
- name: Print go version
run: go version
- name: deps
run: make -w grpc
- name: daemon
run: make -w daemon-codestyle
- name: service
run: make -wC service codestyle
- name: webui
run: make -w webui-codestyle
run: make -wC webui.dev codestyle

View File

@@ -23,12 +23,12 @@ jobs:
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: devskim-results.sarif

View File

@@ -0,0 +1,73 @@
---
name: Issue Responsibility
on:
issue_comment:
types: [created]
jobs:
update-responsibility-labels:
runs-on: ubuntu-latest
steps:
- name: Update responsibility labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const commentAuthor = context.payload.comment.user.login;
const issueNumber = context.payload.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const skipAction = context.payload.comment.body.includes("/skip-responsibility");
if (skipAction) {
core.info("Skipping responsibility label update");
return;
}
const developers = ["jamesread"]
const commenterIsDeveloper = developers.includes(commentAuthor);
const commenterIsUser = !commenterIsDeveloper;
const issueLabels = context.payload.issue.labels.map(label => label.name);
if (issueLabels.includes("waiting-on-developer")) {
if (commenterIsDeveloper) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "waiting-on-developer",
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["waiting-on-requestor"],
});
core.info(`Switched responsibility to user for issue #${issueNumber}`);
}
}
if (issueLabels.includes("waiting-on-requestor")) {
if (commenterIsUser) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "waiting-on-requestor",
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["waiting-on-developer"],
});
core.info(`Switched responsibility to developer for issue #${issueNumber}`);
}
}

11
.gitignore vendored
View File

@@ -1,10 +1,10 @@
**/*.swp
**/*.swo
gen/
/OliveTin
/OliveTin.armhf
/OliveTin.exe
reports
service/gen/
service/OliveTin
service/OliveTin.armhf
service/OliveTin.exe
service/reports
releases/
dist/
installation-id.txt
@@ -12,3 +12,4 @@ tmp/
webui/
webui.dev/node_modules
webui.dev/.parcel-cache
custom-webui

View File

@@ -1,28 +1,32 @@
project_name: OliveTin
version: 2
before:
hooks:
- go mod download
- make service-prep
builds:
- env:
- CGO_ENABLED=0
binary: OliveTin
main: main.go
dir: service
goos:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- arm64
- arm
- riscv64
goarm:
- 5 # For old RPIs
- 6
- 7
main: cmd/OliveTin/main.go
ignore:
- goos: darwin
goarch: arm # Mac does not work on [32bit] arm
@@ -39,7 +43,7 @@ builds:
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Branch }}-{{ .ShortCommit }}"
version_template: "{{ .Branch }}-{{ .ShortCommit }}"
changelog:
sort: asc
groups:
@@ -62,25 +66,19 @@ changelog:
- '^refactor:'
archives:
-
format: tar.gz
- formats: tar.gz
files:
- config.yaml
- LICENSE
- README.md
- Dockerfile
- webui
- OliveTin.service
- ./var/
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
wrap_in_directory: true
format_overrides:
- goos: windows
format: zip
formats: zip
dockers:
- image_templates:
@@ -98,6 +96,7 @@ dockers:
- webui
- var/entities/
- config.yaml
- var/helper-actions/
- image_templates:
- "docker.io/jamesread/olivetin:{{ .Tag }}-arm64"
@@ -114,6 +113,7 @@ dockers:
- webui
- var/entities/
- config.yaml
- var/helper-actions/
docker_manifests:
- name_template: docker.io/jamesread/olivetin:{{ .Version }}
@@ -151,7 +151,7 @@ nfpms:
file_name_template: '{{ .PackageName }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
contents:
- src: OliveTin.service
- src: var/systemd/OliveTin.service
dst: /etc/systemd/system/OliveTin.service
- src: webui/*

View File

@@ -1,10 +1,19 @@
---
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
# Alternative semantic commit checker
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.1.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [] # optional: list of Conventional Commits types to allow e.g. [feat, fix, ci, chore, test]

View File

@@ -11,6 +11,14 @@ Ideas may be discussed, purely on their merits and issues. Our Code of Conduct
discussion throughout the whole process. This project respects the
link:https://www.kernel.org/doc/html/latest/process/code-of-conduct.html[Linux Kernel code of conduct].
== More than 3 lines - talk to someone first
If you're planning on making a change that's more than a 3 lines, please talk to someone first. This is so that you don't waste your time on something that might not be accepted. It's also a good way to get some feedback on your idea and make sure you're on the right track.
== A PR should be one logical change
Please try to keep your pull requests small and focused. It's almost impossible to review PRs that change lots of files for lots of different reasons. If you have a big change, it's probably best to break it down into smaller, more manageable chunks, otherwise it's likely to be rejected.
== If you're not sure, ask!
Don't be afraid to ask for advice before working on a
@@ -22,13 +30,21 @@ the general direction and roadmap of this project without asking.
The preferred way to communicate is probably via Discord or GitHub issues.
=== Dev environment setup and clean build - Fedora
=== Dev environment setup and clean build
```
# Step1: setup compile env
# - Fedora
dnf install git go protobuf-compiler make -y
# - Windows with chocolatey
choco install git go protoc make python nodejs-lts -y
# Step2: clone and setup repo
git clone https://github.com/OliveTin/OliveTin.git
cd OliveTin
make githooks
# Step3: compile binary for current dev env (OS, ARCH)
# `make grpc` will also run `make go-tools`, which installs "buf". This binary
# will be put in your GOPATH/bin/, which should be on your path. buf is used to
# generate the protobuf / grpc stubs.

View File

@@ -1,7 +1,13 @@
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:38-x86_64
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:40-x86_64 AS olivetin-tmputils
RUN microdnf -y install dnf-plugins-core && \
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:40-x86_64
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
LABEL org.opencontainers.image.title=OliveTin
LABEL org.opencontainers.image.title OliveTin
RUN mkdir -p /config /config/entities/ /var/www/olivetin \
&& \
@@ -11,9 +17,17 @@ RUN mkdir -p /config /config/entities/ /var/www/olivetin \
shadow-utils \
apprise \
jq \
docker \
git \
&& microdnf clean all
COPY --from=olivetin-tmputils \
/usr/bin/docker \
/usr/bin/docker
COPY --from=olivetin-tmputils \
/usr/libexec/docker/cli-plugins/docker-compose \
/usr/libexec/docker/cli-plugins/docker-compose
RUN useradd --system --create-home olivetin -u 1000
EXPOSE 1337/tcp
@@ -24,6 +38,7 @@ VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
COPY var/helper-actions/* /usr/bin/
USER olivetin

View File

@@ -1,19 +1,33 @@
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:38-aarch64
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:40-aarch64 AS olivetin-tmputils
RUN microdnf -y install dnf-plugins-core && \
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:40-aarch64
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
LABEL org.opencontainers.image.title=OliveTin
LABEL org.opencontainers.image.title OliveTin
RUN mkdir -p /config /config/entities/ /var/www/olivetin \
&& \
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
iputils \
openssh-clients \
shadow-utils \
apprise \
jq \
docker \
git \
&& microdnf clean all
COPY --from=olivetin-tmputils \
/usr/bin/docker \
/usr/bin/docker
COPY --from=olivetin-tmputils \
/usr/libexec/docker/cli-plugins/docker-compose \
/usr/libexec/docker/cli-plugins/docker-compose
RUN useradd --system --create-home olivetin -u 1000
EXPOSE 1337/tcp
@@ -24,6 +38,7 @@ VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
COPY var/helper-actions/* /usr/bin/
USER olivetin

View File

@@ -20,6 +20,7 @@ VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
COPY var/helper-actions/* /usr/bin/
USER olivetin

50
Jenkinsfile vendored
View File

@@ -1,50 +0,0 @@
pipeline {
agent any
options {
skipDefaultCheckout(true)
}
stages {
stage ('Pre-Build') {
steps {
cleanWs()
checkout scm
sh 'make go-tools'
}
}
stage('Compile') {
steps {
withEnv(["PATH+GO=/root/go/bin/"]) {
sh 'go env'
sh 'echo $PATH'
sh 'buf generate'
sh 'make daemon-compile'
}
}
}
stage ('Post-Compile') {
parallel {
stage('Codestyle') {
steps {
withEnv(["PATH+GO=/root/go/bin/"]) {
sh 'make daemon-codestyle'
sh 'make webui-codestyle'
}
}
}
stage('UnitTests') {
steps {
withEnv(["PATH+GO=/root/go/bin/"]) {
sh 'make daemon-unittests'
}
}
}
}
}
}
}

View File

@@ -1,45 +1,26 @@
compile: daemon-compile-x64-lin
define delete-files
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
endef
daemon-compile-armhf:
GOARCH=arm GOARM=6 go build -o OliveTin.armhf github.com/OliveTin/OliveTin/cmd/OliveTin
service:
$(MAKE) -wC service
daemon-compile-x64-lin:
GOOS=linux go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin
daemon-compile-x64-win:
GOOS=windows GOARCH=amd64 go build -o OliveTin.exe github.com/OliveTin/OliveTin/cmd/OliveTin
daemon-compile: daemon-compile-armhf daemon-compile-x64-lin daemon-compile-x64-win
daemon-codestyle:
go fmt ./...
go vet ./...
gocyclo -over 4 cmd internal
gocritic check ./...
daemon-unittests:
mkdir -p reports
go test ./... -coverprofile reports/unittests.out
go tool cover -html=reports/unittests.out -o reports/unittests.html
service-prep:
$(MAKE) -wC service prep
service-unittests:
$(MAKE) -wC service unittests
it:
cd integration-tests && make
githooks:
cp -v .githooks/* .git/hooks/
$(MAKE) -wC integration-tests
go-tools:
go install "github.com/bufbuild/buf/cmd/buf"
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
go install "github.com/go-critic/go-critic/cmd/gocritic"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
go install "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
go install "google.golang.org/protobuf/cmd/protoc-gen-go"
$(MAKE) -wC service go-tools
proto: grpc
grpc: go-tools
buf generate
$(MAKE) -wC proto
dist: protoc
@@ -67,17 +48,22 @@ devrun: compile
devcontainer: compile podman-image podman-container
webui-codestyle:
cd webui.dev && npm install
cd webui.dev && ./node_modules/.bin/eslint main.js js/*
cd webui.dev && ./node_modules/.bin/stylelint style.css
make -wC webui.dev codestyle
webui-dist:
rm -rf webui webui.dev/dist
$(call delete-files,webui)
$(call delete-files,webui.dev/dist)
cd webui.dev && npm install
cd webui.dev && parcel build --public-url "." && mv dist ../webui
cp webui.dev/*.png webui/
cd webui.dev && npx parcel build --public-url "."
python -c "import shutil;shutil.move('webui.dev/dist', 'webui')"
python -c "import shutil;import glob;[shutil.copy(f, 'webui') for f in glob.glob('webui.dev/*.png')]"
clean:
rm -rf dist OliveTin OliveTin.armhf OliveTin.exe reports gen
$(call delete-files,dist)
$(call delete-files,OliveTin)
$(call delete-files,OliveTin.armhf)
$(call delete-files,OliveTin.exe)
$(call delete-files,reports)
$(call delete-files,gen)
.PHONY: grpc
.PHONY: grpc proto service

View File

@@ -4,6 +4,7 @@
OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface.
[![Maturity Badge](https://img.shields.io/badge/maturity-Production-brightgreen)](#none)
[![Discord](https://img.shields.io/discord/846737624960860180?label=Discord%20Server)](https://discord.gg/jhYWWpNJ3v)
[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/awesome-selfhosted/awesome-selfhosted#automation)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
@@ -14,6 +15,8 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotDesktop.png" />
<a href = "#screenshots">More screenshots below</a>
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
## Use cases
**Safely** give access to commands, for less technical people;
@@ -67,56 +70,6 @@ Mobile screen size (responsive layout);
## Documentation
All documentation can be found at http://docs.olivetin.app . This includes installation and usage guide, etc.
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
### Quickstart reference for `config.yaml`
This is a quick example of `config.yaml` - but again, lots of documentation for how to write your `config.yaml` can be found at [the documentation site.](https://docs.olivetin.app)
* (Recommended) [Linux package install (.rpm/.deb)](https://docs.olivetin.app/install-linuxpackage.html) install instructions
* [Container (podman/docker)](https://docs.olivetin.app/install-container.html) install instructions
* [Docker compose](https://docs.olivetin.app/install-compose.html) install instructions
* [Helm on Kubernetes](https://docs.olivetin.app/install-helm.html) install instructions
* [Kubernetes (manual)](https://docs.olivetin.app/install-k8s.html) install instructions
* [.tar.gz (manual)](https://docs.olivetin.app/install-targz.html) install instructions
Put this `config.yaml` in `/etc/OliveTin/` if you're running a standard service, or mount it at `/config` if running in a container.
```yaml
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
logLevel: "INFO"
# Actions (buttons) to show up on the WebUI:
actions:
# Docs: https://docs.olivetin.app/action-container-control.html
- title: Restart Plex
icon: restart
shell: docker restart plex
# This will send 1 ping
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping host
shell: ping {{ host }} -c {{ count }}
icon: ping
arguments:
- name: host
title: host
type: ascii_identifier
default: example.com
- name: count
title: Count
type: int
default: 1
# Restart http on host "webserver1"
# Docs: https://docs.olivetin.app/action-ssh.html
- title: restart httpd
icon: restart
shell: ssh root@webserver1 'service httpd restart'
```
A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/config.yaml).
You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **MacOS** and other platforms, too!

View File

@@ -1,7 +0,0 @@
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: e9fcfb66f77242e5b8fd4564d7a01033

View File

@@ -5,20 +5,22 @@
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
logLevel: "INFO"
# Checking for updates https://docs.olivetin.app/reference/updateChecks.html
checkForUpdates: false
# Actions are commands that are executed by OliveTin, and normally show up as
# buttons on the WebUI.
#
# Docs: https://docs.olivetin.app/create-your-first-action.html
# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
actions:
# This is the most simple action, it just runs the command and flashes the
# button to indicate status.
#
# If you are running OliveTin in a container remember to pass through the
# docker socket! https://docs.olivetin.app/action-container-control.html
# docker socket! https://docs.olivetin.app/solutions/container-control-panel/index.html
- title: Ping the Internet
shell: ping -c 3 1.1.1.1
icon: ping
@@ -40,11 +42,16 @@ actions:
# This uses `popupOnStart: execution-button` to display a mini button that
# links to the logs.
#
# You can also rate-limit actions too.
- title: date
shell: date
timeout: 6
icon: clock
popupOnStart: execution-button
maxRate:
- limit: 3
duration: 5m
# You are not limited to operating system commands, and of course you can run
# your own scripts. Here `maxConcurrent` stops the script running multiple
@@ -52,7 +59,7 @@ actions:
# runs for too long.
- title: Run backup script
shell: /opt/backupScript.sh
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ stdout }} '"
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '"
maxConcurrent: 1
timeout: 10
icon: backup
@@ -61,8 +68,9 @@ actions:
# When you want to prompt users for input, that is when you should use
# `arguments` - this presents a popup dialog and asks for argument values.
#
# Docs: https://docs.olivetin.app/action-ping.html
# Docs: https://docs.olivetin.app/action_examples/ping.html
- title: Ping host
id: ping_host
shell: ping {{ host }} -c {{ count }}
icon: ping
timeout: 100
@@ -85,7 +93,7 @@ actions:
# However, if you are running in a container you will need to do some setup,
# see the docs below.
#
# Docs: https://docs.olivetin.app/action-container-control.html
# Docs: https://docs.olivetin.app/solutions/container-control-panel/index.html
- title: Restart Docker Container
icon: restart
shell: docker restart {{ container }}
@@ -100,25 +108,56 @@ actions:
# There is a special `confirmation` argument to help against accidental clicks
# on "dangerous" actions.
#
# Docs: https://docs.olivetin.app/confirmation.html
# Docs: https://docs.olivetin.app/args/input_confirmation.html
- title: Delete old backups
icon: ashtonished
shell: rm -rf /opt/oldBackups/
arguments:
- type: html
title: Description
default:
The documentation for this action can be found at <a href = "example.com">example.com</a>.
- type: confirmation
title: Are you sure?!
# This is an action that runs a script included with OliveTin, that will
# download themes. You will still need to set theme "themeName" in your config.
#
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
- title: Get OliveTin Theme
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
icon: theme
arguments:
- name: themeGitRepo
title: Theme's Git Repository
description: Find new themes at https://olivetin.app/themes
type: url
- name: themeFolderName
title: Theme's Folder Name
type: ascii_identifier
# Sometimes you want to run actions on other servers - don't overcomplicate
# it, just use SSH!
# it, just use SSH! OliveTin includes a helper to make this easier, which is
# entirely optional. You can also setup SSH manually.
#
# Docs: https://docs.olivetin.app/action-ssh.html
# Docs: https://docs.olivetin.app/action-service.html
# Docs: https://docs.olivetin.app/action_examples/ssh-easy.html
# Docs: https://docs.olivetin.app/action_examples/ssh-manual.html
- title: "Setup easy SSH"
icon: ssh
shell: olivetin-setup-easy-ssh
popupOnStart: execution-dialog
# Here's how to use SSH with the "easy" config, to restart a service on
# another server.
#
# Docs: https://docs.olivetin.app/action_examples/ssh-easy.html
# Docs: https://docs.olivetin.app/action_examples/systemd_service.html
- title: Restart httpd on server1
id: restart_httpd
icon: restart
timeout: 1
shell: ssh root@server1 'service httpd restart'
shell: ssh -F /config/ssh/easy.cfg root@server1 'service httpd restart'
# Lots of people use OliveTin to build web interfaces for their electronics
# projects. It's best to install OliveTin as a native package (eg, .deb), and
@@ -131,19 +170,19 @@ actions:
# can also just specify any HTML, this includes any unicode character,
# or a <img = "..." /> link to a custom icon.
#
# Docs: https://docs.olivetin.app/icons.html
# Docs: https://docs.olivetin.app/action_customization/icons.html
#
# Lots of people use OliveTin to easily execute ansible-playbooks. You
# probably want a much longer timeout as well (so that ansible completes).
#
# Docs: https://docs.olivetin.app/ansible-playbook.html
# Docs: https://docs.olivetin.app/action_examples/ansible.html
- title: "Run Automation Playbook"
icon: '&#129302;'
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
timeout: 120
# The following actions are "dummy" actions, used in a Dashboard. As long as
# you have these referenced in a dashboard, they will not who up in the
# you have these referenced in a dashboard, they will not show up in the
# `actions` view.
- title: Ping hypervisor1
shell: echo "hypervisor1 online"
@@ -167,13 +206,13 @@ actions:
icon: box
shell: docker start {{ container.Names }}
entity: container
trigger: Update container entity file
triggers: ["Update container entity file"]
- title: Stop {{ container.Names }}
icon: box
shell: docker stop {{ container.Names }}
entity: container
trigger: Update container entity file
triggers: ["Update container entity file"]
# Lastly, you can hide actions from the web UI, this is useful for creating
# background helpers that execute only on startup or a cron, for updating
@@ -203,13 +242,13 @@ actions:
# in your configuration as variables. For example; `container.status`,
# or `vm.hostname`.
#
# Docs: http://docs.olivetin.app/entities.html
# Docs: https://docs.olivetin.app/entities/intro.html
entities:
# YAML files are the default expected format, so you can use .yml or .yaml,
# or even .txt, as long as the file contains valid a valid yaml LIST, then it
# will load properly.
#
# Docs: https://docs.olivetin.app/entities.html
# Docs: https://docs.olivetin.app/entities/intro.html
- file: entities/servers.yaml
name: server
@@ -221,20 +260,25 @@ entities:
#
# The only way to properly use entities, are to use them with a `fieldset` on
# a dashboard.
#
# Docs: https://docs.olivetin.app/dashboards/intro.html
dashboards:
# Top level items are dashboards.
- title: My Servers
contents:
# The contents of a dashboard will try to look for an action with a
# matching title IF the `contents: ` property is empty.
- title: Ping All Servers
# If you create an item with some "contents:", OliveTin will show that as
# directory.
- title: Hypervisors
- title: All Servers
type: fieldset
contents:
- title: Ping hypervisor1
- title: Ping hypervisor2
# The contents of a dashboard will try to look for an action with a
# matching title IF the `contents: ` property is empty.
- title: Ping All Servers
# If you create an item with some "contents:", OliveTin will show that as
# directory.
- title: Hypervisors
contents:
- title: Ping hypervisor1
- title: Ping hypervisor2
# If you specify `type: fieldset` and some `contents`, it will show your
# actions grouped together without a folder.

133
go.mod
View File

@@ -1,133 +0,0 @@
module github.com/OliveTin/OliveTin
go 1.21
toolchain go1.21.9
require (
github.com/MicahParks/keyfunc/v3 v3.3.2
github.com/bufbuild/buf v1.30.1
github.com/fsnotify/fsnotify v1.6.0
github.com/fzipp/gocyclo v0.6.0
github.com/go-critic/go-critic v0.11.1
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.4.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
github.com/prometheus/client_golang v1.19.0
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.9.0
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa
google.golang.org/grpc v1.62.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
google.golang.org/protobuf v1.33.0
gopkg.in/yaml.v3 v3.0.1
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1 // indirect
connectrpc.com/connect v1.16.0 // indirect
connectrpc.com/otelconnect v0.7.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/MicahParks/jwkset v0.5.17 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protocompile v0.9.0 // indirect
github.com/bufbuild/protovalidate-go v0.6.0 // indirect
github.com/bufbuild/protoyaml-go v0.1.8 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cristalhq/acmd v0.11.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v26.0.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v26.0.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/fgprof v0.9.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/pkgload v1.2.2 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-containerregistry v0.19.1 // indirect
github.com/google/pprof v0.0.0-20240327155427-868f304927ed // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jdx/go-netrc v1.0.0 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quasilyte/go-ruleguard v0.4.2 // 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/rs/cors v1.10.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

View File

@@ -4,7 +4,11 @@ test-install:
npm install --no-fund
test-run:
./node_modules/.bin/mocha -t 10000
npx mocha -t 10000
find-flakey-tests:
echo "Running test-run infinately"
sh -c "while make test-run; do :; done"
nginx:
podman-compose up -d nginx

View File

@@ -24,12 +24,12 @@ actions:
shell: sleep 5
icon: "&#x1F62A"
- title: date-popup
shell: date
- title: dir-popup
shell: dir
popupOnStart: execution-dialog-stdout-only
- title: date-passive
shell: date
- title: cd-passive
shell: cd
- title: "Run Ansible Playbook"
icon: "&#x1F1E6"

View File

@@ -0,0 +1,16 @@
#
# Integration Test Config: General
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
prometheus:
enabled: true
defaultGoMetrics: false
actions:
- title: Hello OliveTin
shell: echo "Hello OliveTin"

View File

@@ -0,0 +1,14 @@
# Integration Test Config: Sleep
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: Sleep
shell: sleep 10
popupOnStart: execution-dialog
timeout: 9

View File

@@ -1,27 +1,71 @@
import { By } from 'selenium-webdriver'
import fs from 'fs'
import { expect } from 'chai'
import { Condition } from 'selenium-webdriver'
export async function getActionButtons (webdriver) {
return await webdriver.findElement(By.id('contentActions')).findElements(By.tagName('button'))
}
export function takeScreenshot (webdriver) {
export function takeScreenshotOnFailure (test, webdriver) {
if (test.state === 'failed') {
const title = test.fullTitle();
console.log(`Test failed, taking screenshot: ${title}`);
takeScreenshot(webdriver, title);
}
}
export function takeScreenshot (webdriver, title) {
return webdriver.takeScreenshot().then((img) => {
fs.writeFileSync('out.png', img, 'base64')
fs.mkdirSync('screenshots', { recursive: true });
title = title.replaceAll(/[\(\)\|\*\<\>\:]/g, "_")
title = 'failed-test.' + title
fs.writeFileSync('screenshots/' + title + '.png', img, 'base64')
})
}
export async function getRootAndWait() {
await webdriver.get(runner.baseUrl())
await webdriver.wait(new Condition('wait for initial-marshal-complete', async function() {
const body = await webdriver.findElement(By.tagName('body'))
const attr = await body.getAttribute('initial-marshal-complete')
await webdriver.get(runner.baseUrl())
await webdriver.wait(new Condition('wait for initial-marshal-complete', async function() {
const body = await webdriver.findElement(By.tagName('body'))
const attr = await body.getAttribute('initial-marshal-complete')
if (attr == 'true') {
return true
} else {
return false
}
}))
if (attr == 'true') {
return true
} else {
return false
}
}))
}
export async function requireExecutionDialogStatus (webdriver, expected) {
// It seems that webdriver will not give us text if domStatus is hidden (which it will be until complete)
await webdriver.executeScript('window.executionDialog.domExecutionDetails.hidden = false')
await webdriver.wait(new Condition('wait for action to be running', async function () {
const actual = await webdriver.executeScript('return window.executionDialog.domStatus.getText()')
if (actual === expected) {
return true
} else {
console.log('Waiting for domStatus text to be: ', expected, ', it is currently: ', actual)
console.log(await webdriver.executeScript('return window.executionDialog.res'))
return false
}
}))
}
export async function findExecutionDialog (webdriver) {
return webdriver.findElement(By.id('execution-results-popup'))
}
export async function getActionButton (webdriver, title) {
const buttons = await webdriver.findElements(By.css('[title="' + title + '"]'))
expect(buttons).to.have.length(1)
return buttons[0]
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,12 @@
"author": "",
"license": "AGPL-3.0-only",
"devDependencies": {
"chai": "^5.1.0",
"eslint": "^8.51.0",
"mocha": "^10.4.0",
"selenium-webdriver": "^4.19.0"
"chai": "^5.2.0",
"eslint": "^9.22.0",
"mocha": "^11.1.0",
"selenium-webdriver": "^4.29.0"
},
"dependencies": {
"wait-on": "^7.2.0"
"wait-on": "^8.0.3"
}
}

View File

@@ -25,6 +25,10 @@ class OliveTinTestRunner {
baseUrl() {
return this.BASE_URL
}
metricsUrl() {
return new URL('metrics', this.baseUrl());
}
}
class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
@@ -32,9 +36,17 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
let stdout = ""
let stderr = ""
this.ot = spawn('./../OliveTin', ['-configdir', 'configs/' + cfg + '/'])
console.log(" OliveTin starting local process...")
const logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
this.ot = spawn('./../service/OliveTin', ['-configdir', 'configs/' + cfg + '/'])
let logStdout = false
if (process.env.CI === 'true') {
logStdout = true;
} else {
logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
}
this.ot.stdout.on('data', (data) => {
stdout += data
@@ -64,6 +76,8 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
if (this.ot.exitCode == null) {
this.BASE_URL = 'http://localhost:1337/'
console.log(" OliveTin waiting for local process to start...")
await waitOn({
resources: [this.BASE_URL]
})
@@ -85,7 +99,12 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
console.log(" OliveTin local process killed")
}
await new Promise((res) => setTimeout(res, 100))
if (process.env.CI === 'true') {
// GitHub runners seem to need a bit more time to clean up
await new Promise((res) => setTimeout(res, 3000))
} else {
await new Promise((res) => setTimeout(res, 100))
}
}
}

View File

@@ -1,7 +1,11 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until } from 'selenium-webdriver'
import { getRootAndWait, takeScreenshot } from '../lib/elements.js'
import {
getRootAndWait,
takeScreenshot,
takeScreenshotOnFailure,
} from '../lib/elements.js'
describe('config: entities', function () {
before(async function () {
@@ -12,6 +16,10 @@ describe('config: entities', function () {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Entity buttons are rendered', async function() {
await getRootAndWait()

View File

@@ -2,7 +2,11 @@ import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until, Condition } from 'selenium-webdriver'
//import * as waitOn from 'wait-on'
import { getRootAndWait, getActionButtons } from '../lib/elements.js'
import {
getRootAndWait,
getActionButtons,
takeScreenshotOnFailure,
} from '../lib/elements.js'
describe('config: general', function () {
before(async function () {
@@ -13,6 +17,10 @@ describe('config: general', function () {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Page title', async function () {
await webdriver.get(runner.baseUrl())
@@ -44,42 +52,42 @@ describe('config: general', function () {
expect(buttons).to.have.length(8)
})
it('Start date action (popup)', async function() {
it('Start dir action (popup)', async function () {
await getRootAndWait()
const buttons = await webdriver.findElements(By.css('[title="date-popup"]'))
const buttons = await webdriver.findElements(By.css('[title="dir-popup"]'))
expect(buttons).to.have.length(1)
const buttonDate = buttons[0]
const buttonCMD = buttons[0]
expect(buttonDate).to.not.be.null
expect(buttonCMD).to.not.be.null
buttonDate.click()
buttonCMD.click()
const dialog = await webdriver.findElement(By.id('execution-results-popup'))
expect(await dialog.isDisplayed()).to.be.true
const title = await webdriver.findElement(By.id('execution-dialog-title'))
expect(await title.getAttribute('innerText')).to.be.equal('date-popup')
expect(await webdriver.wait(until.elementTextIs(title, 'dir-popup'), 2000))
const dialogErr = await webdriver.findElement(By.id('big-error'))
expect(dialogErr).to.not.be.null
expect(await dialogErr.isDisplayed()).to.be.false
})
it('Start date action (passive)', async function() {
it('Start cd action (passive)', async function () {
await getRootAndWait()
const buttons = await webdriver.findElements(By.css('[title="date-passive"]'))
const buttons = await webdriver.findElements(By.css('[title="cd-passive"]'))
expect(buttons).to.have.length(1)
const buttonDate = buttons[0]
const buttonCMD = buttons[0]
expect(buttonDate).to.not.be.null
expect(buttonCMD).to.not.be.null
buttonDate.click()
buttonCMD.click()
const dialog = await webdriver.findElement(By.id('execution-results-popup'))
expect(await dialog.isDisplayed()).to.be.false
@@ -88,6 +96,7 @@ describe('config: general', function () {
expect(await title.getAttribute('innerText')).to.be.equal('?')
const dialogErr = await webdriver.findElement(By.id('big-error'))
console.log("big error is: " + dialogErr.innerHTML)
expect(dialogErr).to.not.be.null
expect(await dialogErr.isDisplayed()).to.be.false
})

View File

@@ -2,6 +2,11 @@ import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By } from 'selenium-webdriver'
import {
getRootAndWait,
getActionButtons,
takeScreenshotOnFailure,
} from '../lib/elements.js'
describe('config: hiddenFooter', function () {
before(async function () {
@@ -12,6 +17,10 @@ describe('config: hiddenFooter', function () {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Check that footer is hidden', async () => {
await webdriver.get(runner.baseUrl())

View File

@@ -1,5 +1,11 @@
import { expect } from 'chai'
import { By } from 'selenium-webdriver'
import {
getRootAndWait,
getActionButtons,
takeScreenshotOnFailure,
} from '../lib/elements.js'
describe('config: hiddenNav', function () {
before(async function () {
@@ -10,6 +16,10 @@ describe('config: hiddenNav', function () {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('nav is hidden', async () => {
await webdriver.get(runner.baseUrl())

View File

@@ -1,7 +1,12 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until } from 'selenium-webdriver'
import { getActionButtons, getRootAndWait } from '../lib/elements.js'
import {
getRootAndWait,
getActionButtons,
takeScreenshotOnFailure,
} from '../lib/elements.js'
describe('config: multipleDropdowns', function () {
before(async function () {
@@ -12,6 +17,10 @@ describe('config: multipleDropdowns', function () {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Multiple dropdowns are possible', async function() {
await getRootAndWait()

View File

@@ -0,0 +1,40 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By } from 'selenium-webdriver'
import {
takeScreenshotOnFailure,
} from '../lib/elements.js'
let metrics = [
{'name': 'olivetin_actions_requested_count', 'type': 'counter', 'desc': 'The actions requested count'},
{'name': 'olivetin_config_action_count', 'type': 'gauge', 'desc': 'The number of actions in the config file'},
{'name': 'olivetin_config_reloaded_count', 'type': 'counter', 'desc': 'The number of times the config has been reloaded'},
{'name': 'olivetin_sv_count', 'type': 'gauge', 'desc': 'The number entries in the sv map'},
]
describe('config: prometheus', function () {
before(async function () {
await runner.start('prometheus')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Metrics are available with correct types', async () => {
webdriver.get(runner.metricsUrl())
const prometheusOutput = await webdriver.findElement(By.tagName('pre')).getText()
expect(prometheusOutput).to.not.be.null
metrics.forEach(({name, type, desc}) => {
const metaLines = `# HELP ${name} ${desc}\n`
+ `# TYPE ${name} ${type}\n`
expect(prometheusOutput).to.match(new RegExp(metaLines))
})
})
})

View File

@@ -0,0 +1,49 @@
import * as process from 'node:process'
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, Condition } from 'selenium-webdriver'
import {
takeScreenshot,
takeScreenshotOnFailure,
findExecutionDialog,
requireExecutionDialogStatus,
getRootAndWait,
getActionButton
} from '../lib/elements.js'
describe('config: sleep', function () {
before(async function () {
await runner.start('sleep')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Sleep action kill', async function() {
await getRootAndWait()
const btnSleep = await getActionButton(webdriver, "Sleep")
const dialog = await findExecutionDialog(webdriver)
expect(await dialog.isDisplayed()).to.be.false
await btnSleep.click()
expect(await dialog.isDisplayed()).to.be.true
await requireExecutionDialogStatus(webdriver, "unknown")
const killButton = await webdriver.findElement(By.id('execution-dialog-kill-action'))
expect(killButton).to.not.be.undefined
await killButton.click()
await requireExecutionDialogStatus(webdriver, "Completed")
})
})

View File

@@ -1,4 +1,8 @@
import { expect } from 'chai'
import {
getRootAndWait,
takeScreenshotOnFailure,
} from '../lib/elements.js'
describe('config: trustedHeader', function () {
before(async function () {
@@ -9,7 +13,13 @@ describe('config: trustedHeader', function () {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('req with X-User', async () => {
await getRootAndWait()
const req = await fetch(runner.baseUrl() + '/api/WhoAmI', {
headers: {
"X-User": "fred",

View File

@@ -1,165 +0,0 @@
package acl
import (
"context"
config "github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
"google.golang.org/grpc/metadata"
)
// User respresents a person.
type AuthenticatedUser struct {
Username string
Usergroup string
acls []string
}
func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action) {
if cfg.LogDebugOptions.AclNotMatched {
log.WithFields(log.Fields{
"User": user.Username,
"Action": action.Title,
}).Debugf("%v - No ACLs Matched", aclFunction)
}
}
func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
if cfg.LogDebugOptions.AclMatched {
log.WithFields(log.Fields{
"User": user.Username,
"Action": action.Title,
"ACL": acl.Name,
}).Debugf("%v - Matched ACL", aclFunction)
}
}
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
func IsAllowedLogs(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
if acl.Permissions.Logs {
logAclMatched(cfg, "isAllowedLogs", user, action, acl)
return true
}
}
logAclNotMatched(cfg, "isAllowedLogs", user, action)
return cfg.DefaultPermissions.Logs
}
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
func IsAllowedExec(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
if acl.Permissions.Exec {
logAclMatched(cfg, "isAllowedExec", user, action, acl)
return true
}
}
logAclNotMatched(cfg, "isAllowedExec", user, action)
return cfg.DefaultPermissions.Exec
}
// IsAllowedView checks if a User is allowed to view an Action
func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
if action.Hidden {
return false
}
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
if acl.Permissions.View {
logAclMatched(cfg, "isAllowedView", user, action, acl)
return true
}
}
logAclNotMatched(cfg, "isAllowedView", user, action)
return cfg.DefaultPermissions.View
}
func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
mdValues := md.Get(key)
if len(mdValues) > 0 {
return mdValues[0]
}
return ""
}
// UserFromContext tries to find a user from a grpc context
func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
md, ok := metadata.FromIncomingContext(ctx)
ret := &AuthenticatedUser{
Username: "guest",
Usergroup: "guest",
}
if ok {
ret.Username = getMetdataKeyOrEmpty(md, "username")
ret.Usergroup = getMetdataKeyOrEmpty(md, "usergroup")
}
buildUserAcls(cfg, ret)
log.WithFields(log.Fields{
"username": ret.Username,
"usergroup": ret.Usergroup,
}).Debugf("UserFromContext")
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 slices.Contains(acl.MatchUsergroups, user.Usergroup) {
user.acls = append(user.acls, acl.Name)
continue
}
}
}
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
if !slices.Contains(user.acls, acl.Name) {
// If the user does not have this ACL, then it is not relevant
return false
}
if acl.AddToEveryAction {
return true
}
if slices.Contains(actionAcls, acl.Name) {
return true
}
return false
}
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *AuthenticatedUser) []*config.AccessControlList {
var ret []*config.AccessControlList
for _, acl := range cfg.AccessControlLists {
if isACLRelevantToAction(cfg, actionAcls, acl, user) {
ret = append(ret, acl)
}
}
return ret
}

View File

@@ -1,138 +0,0 @@
package updatecheck
import (
"bytes"
"encoding/json"
config "github.com/OliveTin/OliveTin/internal/config"
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
"github.com/google/uuid"
"github.com/robfig/cron/v3"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"os"
)
type updateRequest struct {
CurrentVersion string
CurrentCommit string
OS string
Arch string
InstallationID string
InContainer bool
}
// AvailableVersion is updated when checking with the update service.
var AvailableVersion = "none"
// CurrentVersion is set by the main cmd (which is in tern set as a compile constant)
var CurrentVersion = "?"
func installationID(filename string) string {
var content string
contentBytes, err := os.ReadFile(filename)
if err != nil {
fileHandle, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
log.Warnf("Could not read + create installation ID file: %v", err)
return "cant-create"
}
content = uuid.NewString()
fileHandle.WriteString(content)
fileHandle.Close()
} else {
content = string(contentBytes)
_, err := uuid.Parse(content)
if err != nil {
log.Errorf("Invalid installation ID, %v", err)
content = "invalid-installation-id"
}
}
log.WithFields(log.Fields{
"content": content,
"from": filename,
}).Infof("Installation ID")
return content
}
// StartUpdateChecker will start a job that runs periodically, checking
// for updates.
func StartUpdateChecker(currentVersion string, currentCommit string, cfg *config.Config, configDir string) {
CurrentVersion = currentVersion
if !cfg.CheckForUpdates {
log.Warn("Update checking is disabled")
return
}
payload := updateRequest{
CurrentVersion: currentVersion,
CurrentCommit: currentCommit,
OS: installationinfo.Runtime.OS,
Arch: installationinfo.Runtime.Arch,
InstallationID: installationID(configDir + "/installation-id.txt"),
InContainer: installationinfo.Runtime.InContainer,
}
s := cron.New(cron.WithSeconds())
// Several values have been tried here.
// 1st: Every 24h - very spammy.
// 2nd: Every 7d - (168 hours - much more reasonable, but it checks in at the same time/day each week.
// Current: Every 100h is not so spammy, and has the advantage that the checkin time "shifts" hours.
s.AddFunc("@every 100h", func() {
actualCheckForUpdate(payload)
})
go actualCheckForUpdate(payload) // On startup
go s.Start()
}
func doRequest(jsonUpdateRequest []byte) string {
req, err := http.NewRequest("POST", "http://update-check.olivetin.app", bytes.NewBuffer(jsonUpdateRequest))
if err != nil {
log.Errorf("Update check failed %v", err)
return ""
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Errorf("Update check failed %v", err)
return ""
}
newVersion, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
return string(newVersion)
}
func actualCheckForUpdate(payload updateRequest) {
jsonUpdateRequest, err := json.Marshal(payload)
log.Debugf("Update request payload: %+v", payload)
if err != nil {
log.Errorf("Update check failed %v", err)
return
}
AvailableVersion = doRequest(jsonUpdateRequest)
log.WithFields(log.Fields{
"NewVersion": AvailableVersion,
}).Infof("Update check complete")
}

4
proto/Makefile Normal file
View File

@@ -0,0 +1,4 @@
buf:
buf generate
.PHONY: buf

View File

@@ -1,15 +1,15 @@
version: v1
plugins:
- name: go
out: gen/grpc/
out: ../service/gen/grpc/
opt: paths=source_relative
- name: go-grpc
out: gen/grpc/
out: ../service/gen/grpc/
opt: paths=source_relative,require_unimplemented_servers=false
- name: grpc-gateway
out: gen/grpc/
out: ../service/gen/grpc/
opt: paths=source_relative
# - name: swagger

8
proto/buf.lock Normal file
View File

@@ -0,0 +1,8 @@
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: 751cbe31638d43a9bfb6162cd2352e67
digest: shake256:87f55470d9d124e2d1dedfe0231221f4ed7efbc55bc5268917c678e2d9b9c41573a7f9a557f6d8539044524d9fc5ca8fbb7db05eb81379d168285d76b57eb8a4

View File

@@ -3,8 +3,7 @@ deps:
- buf.build/googleapis/googleapis
lint:
use:
- DEFAULT
build:
- STANDARD
breaking:
use:
- FILE

View File

@@ -1,6 +1,8 @@
syntax = "proto3";
option go_package = "gen/grpc";
package olivetin.api.v1;
option go_package = "github.com/jamesread/OliveTin/gen/grpc/olivetin/api/v1;apiv1";
import "google/api/annotations.proto";
import "google/api/httpbody.proto";
@@ -46,6 +48,7 @@ message GetDashboardComponentsResponse {
repeated DashboardComponent dashboards = 4;
string authenticated_user = 5;
string authenticated_user_provider = 6;
}
message GetDashboardComponentsRequest {}
@@ -55,6 +58,7 @@ message DashboardComponent {
string type = 2;
repeated DashboardComponent contents = 3;
string icon = 4;
string css_class = 5;
}
message StartActionRequest {
@@ -76,6 +80,8 @@ message StartActionResponse {
message StartActionAndWaitRequest {
string action_id = 1;
repeated StartActionArgument arguments = 2;
}
message StartActionAndWaitResponse {
@@ -98,7 +104,9 @@ message StartActionByGetAndWaitResponse {
LogEntry log_entry = 1;
}
message GetLogsRequest{};
message GetLogsRequest{
int64 start_offset = 1;
};
message LogEntry {
string datetime_started = 1;
@@ -116,10 +124,13 @@ message LogEntry {
bool execution_started = 14;
bool execution_finished = 15;
bool blocked = 16;
int64 datetime_index = 17;
}
message GetLogsResponse {
repeated LogEntry logs = 1;
int64 count_remaining = 2;
int64 page_size = 3;
}
message ValidateArgumentTypeRequest {
@@ -153,6 +164,12 @@ message WhoAmIRequest {}
message WhoAmIResponse {
string authenticated_user = 1;
string usergroup = 2;
string provider = 3;
repeated string acls = 4;
string sid = 5;
}
message SosReportRequest {}
@@ -197,6 +214,10 @@ message EventExecutionFinished {
LogEntry log_entry = 1;
}
message EventExecutionStarted {
LogEntry log_entry = 1;
}
message KillActionRequest {
string execution_tracking_id = 1;
}
@@ -208,6 +229,24 @@ message KillActionResponse {
bool found = 4;
}
message LocalUserLoginRequest {
string username = 1;
string password = 2;
}
message LocalUserLoginResponse {
bool success = 1;
}
message PasswordHashRequest {
string password = 1;
}
message PasswordHashResponse {
}
message LogoutRequest {}
service OliveTinApiService {
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
option (google.api.http) = {
@@ -297,4 +336,24 @@ service OliveTinApiService {
get: "/api/readyz"
};
}
rpc LocalUserLogin(LocalUserLoginRequest) returns (LocalUserLoginResponse) {
option (google.api.http) = {
post: "/api/LocalUserLogin"
body: "*"
};
}
rpc PasswordHash(PasswordHashRequest) returns (google.api.HttpBody) {
option (google.api.http) = {
post: "/api/PasswordHash"
body: "*"
};
}
rpc Logout(LogoutRequest) returns (google.api.HttpBody) {
option (google.api.http) = {
get: "/api/Logout"
};
}
}

View File

@@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./OliveTin"
cmd = "go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin"
cmd = "go build -o OliveTin"
delay = 1
exclude_dir = ["assets", "tmp", "vendor", "testdata", "webui.dev", "webui"]
exclude_file = []

48
service/Makefile Normal file
View File

@@ -0,0 +1,48 @@
define delete-files
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
endef
compile-currentenv:
go build
prep:
go mod download
go generate ./...
compile-armhf:
go env -w GOARCH=arm GOARM=6
go build -o OliveTin.armhf
go env -u GOARCH GOARM
compile-x64-lin:
go env -w GOOS=linux
go build -o OliveTin
go env -u GOOS
compile-x64-win:
go env -w GOOS=windows GOARCH=amd64
go build -o OliveTin.exe
go env -u GOOS GOARCH
compile: compile-armhf compile-x64-lin compile-x64-win
codestyle:
go fmt ./...
go vet ./...
gocyclo -over 4 internal
gocritic check ./...
unittests:
$(call delete-files,reports)
mkdir reports
go test ./... -coverprofile reports/unittests.out
go tool cover -html=reports/unittests.out -o reports/unittests.html
go-tools:
go install "github.com/bufbuild/buf/cmd/buf"
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
go install "github.com/go-critic/go-critic/cmd/gocritic"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
go install "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
go install "google.golang.org/protobuf/cmd/protoc-gen-go"

3
service/generate.go Normal file
View File

@@ -0,0 +1,3 @@
//go:generate make -wC ../
package main

165
service/go.mod Normal file
View File

@@ -0,0 +1,165 @@
module github.com/OliveTin/OliveTin
go 1.23.0
toolchain go1.23.7
require (
github.com/MicahParks/keyfunc/v3 v3.3.2
github.com/alexedwards/argon2id v1.0.0
github.com/bufbuild/buf v1.50.1
github.com/fsnotify/fsnotify v1.6.0
github.com/fzipp/gocyclo v0.6.0
github.com/go-critic/go-critic v0.13.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.4.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
github.com/prometheus/client_golang v1.20.2
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/oauth2 v0.27.0
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4
google.golang.org/grpc v1.71.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
google.golang.org/protobuf v1.36.5
gopkg.in/yaml.v3 v3.0.1
)
require (
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1 // indirect
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 // indirect
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1 // indirect
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 // indirect
buf.build/go/bufplugin v0.7.0 // indirect
buf.build/go/protoyaml v0.3.1 // indirect
buf.build/go/spdx v0.2.0 // indirect
cel.dev/expr v0.19.2 // indirect
connectrpc.com/connect v1.18.1 // indirect
connectrpc.com/otelconnect v0.7.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/MicahParks/jwkset v0.7.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/protoplugin v0.0.0-20250106231243-3a819552c9d9 // indirect
github.com/bufbuild/protovalidate-go v0.8.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // 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 v27.5.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.0.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/pkgload v1.2.2 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/cel-go v0.24.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.20.3 // indirect
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
github.com/hashicorp/hcl v1.0.0 // 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/pgzip v1.2.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/mount v0.3.4 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/reexec v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.4 // 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.50.0 // 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.4.1 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/vbatts/tar-split v0.12.1 // 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.59.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
pluginrpc.com/pluginrpc v0.5.0 // indirect
)

View File

@@ -1,5 +1,21 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1 h1:0nWhrRcnkgw1kwJ7xibIO8bqfOA7pBzBjGCDBxIHch8=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1/go.mod h1:Tgn5bgL220vkFOI0KPStlcClPeOJzAv4uT+V8JXGUnw=
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1 h1:p5SFT60M93aMQhOz81VH3kPg8t1pp/Litae/1eSxie4=
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1/go.mod h1:umI0o7WWHv8lCbLjYUMzfjHKjyaIt2D89sIj1D9fqy0=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1 h1:yeaeyw0RQUe009ebxBQ3TsqBPptiNEGsiS10t+8Htuo=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1/go.mod h1:novQBstnxcGpfKf8qGRATqn1anQKwMJIbH5Q581jibU=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 h1:1SDs5tEGoWWv2vmKLx2B0Bp+yfhlxiU4DaZUII8+Pvs=
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1/go.mod h1:o2AgVM1j3MczvxnMqfZTpiqGwK1VD4JbEagseY0QcjE=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1 h1:uKJgSNHvwQUZ6+0dSnx9MtkZ+h/ORbkKym0rlzIjUSI=
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1/go.mod h1:Ua59W2s7uwPS5sGNgW08QewjBaPnUxOdpkWsuDvJ36Q=
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 h1:XmYgi9W/9oST2ZrfT3ucGWkzD9+Vd0ls9yhyZ8ae0KQ=
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1/go.mod h1:cxFpqWIC80Wm8YNo1038ocBmrF84uQ0IfL0uVdAu9ZY=
buf.build/go/bufplugin v0.7.0 h1:Tq8FXBVfpMxhl3QR6P/gMQHROg1Ss7WhpyD4QVV61ds=
buf.build/go/bufplugin v0.7.0/go.mod h1:LuQzv36Ezu2zQIQUtwg4WJJFe58tXn1anL1IosAh6ik=
buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4=
buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg=
buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4=
cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -37,38 +53,42 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
connectrpc.com/connect v1.16.0 h1:rdtfQjZ0OyFkWPTegBNcH7cwquGAN1WzyJy80oFNibg=
connectrpc.com/connect v1.16.0/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw=
connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY=
connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc=
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
connectrpc.com/otelconnect v0.7.1 h1:scO5pOb0i4yUE66CnNrHeK1x51yq0bE0ehPg6WvzXJY=
connectrpc.com/otelconnect v0.7.1/go.mod h1:dh3bFgHBTb2bkqGCeVVOtHJreSns7uu9wwL2Tbz17ms=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/MicahParks/jwkset v0.5.17 h1:DrcwyKwSP5adD0G2XJTvDulnWXjD6gbjROMgMXDbkKA=
github.com/MicahParks/jwkset v0.5.17/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY=
github.com/MicahParks/jwkset v0.7.0 h1:CXWuiYBk5NuTl+N/3UI3UcYNH79yWuKAZWZkc/y+7Ok=
github.com/MicahParks/jwkset v0.7.0/go.mod h1:fVrj6TmG1aKlJEeceAz7JsXGTXEn72zP1px3us53JrA=
github.com/MicahParks/keyfunc/v3 v3.3.2 h1:YTtwc4dxalBZKFqHhqctBWN6VhbLdGhywmne9u5RQVM=
github.com/MicahParks/keyfunc/v3 v3.3.2/go.mod h1:GJBeEjnv25OnD9y2OYQa7ELU6gYahEMBNXINZb+qm34=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
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=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
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.30.1 h1:QFtanwsXodoGFAwzXFXGXpzBkb7N2u8ZDyA3jWB4Pbs=
github.com/bufbuild/buf v1.30.1/go.mod h1:7W8DJnj76wQa55EA3z2CmDxS0/nsHh8FqtE00dyDAdA=
github.com/bufbuild/protocompile v0.9.0 h1:DI8qLG5PEO0Mu1Oj51YFPqtx6I3qYXUAhJVJ/IzAVl0=
github.com/bufbuild/protocompile v0.9.0/go.mod h1:s89m1O8CqSYpyE/YaSGtg1r1YFMF5nLTwh4vlj6O444=
github.com/bufbuild/protovalidate-go v0.6.0 h1:Jgs1kFuZ2LHvvdj8SpCLA1W/+pXS8QSM3F/E2l3InPY=
github.com/bufbuild/protovalidate-go v0.6.0/go.mod h1:1LamgoYHZ2NdIQH0XGczGTc6Z8YrTHjcJVmiBaar4t4=
github.com/bufbuild/protoyaml-go v0.1.8 h1:X9QDLfl9uEllh4gsXUGqPanZYCOKzd92uniRtW2OnAQ=
github.com/bufbuild/protoyaml-go v0.1.8/go.mod h1:R8vE2+l49bSiIExP4VJpxOXleHE+FDzZ6HVxr3cYunw=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/bufbuild/buf v1.50.1 h1:3sEaWLw6g7bSIJ+yKo6ERF3qpkaLNGd8SzImFpA5gUI=
github.com/bufbuild/buf v1.50.1/go.mod h1:LqTlfsFs4RD3L+VoBudEWJzWi12Pa0+Q2vDQnY0YQv0=
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/protoplugin v0.0.0-20250106231243-3a819552c9d9 h1:kAWER21DzhzU7ys8LL1WkSfbGkwXv+tM30hyEsYrW2k=
github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
github.com/bufbuild/protovalidate-go v0.8.2 h1:sgzXHkHYP6HnAsL2Rd3I1JxkYUyEQUv9awU1PduMxbM=
github.com/bufbuild/protovalidate-go v0.8.2/go.mod h1:K6w8iPNAXBoIivVueSELbUeUl+MmeTQfCDSug85pn3M=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
@@ -82,30 +102,31 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
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.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cristalhq/acmd v0.11.2 h1:ITIWtBRiYbmzk+i8xQgH2RzfCVMII+dOd0CtGWVIhaU=
github.com/cristalhq/acmd v0.11.2/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
github.com/cristalhq/acmd v0.12.0 h1:RdlKnxjN+txbQosg8p/TRNZ+J1Rdne43MVQZ1zDhGWk=
github.com/cristalhq/acmd v0.12.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
github.com/docker/cli v27.5.1+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 v26.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE=
github.com/docker/docker v26.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -116,11 +137,11 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88=
github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
@@ -129,18 +150,20 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-critic/go-critic v0.11.1 h1:/zBseUSUMytnRqxjlsYNbDDxpu3R2yH8oLXo/FOE8b8=
github.com/go-critic/go-critic v0.11.1/go.mod h1:aZVQR7+gazH6aDEQx4356SD7d8ez8MipYjXbEl5JAKA=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
@@ -163,14 +186,12 @@ github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCs
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -198,13 +219,12 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -214,12 +234,11 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY=
github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -235,8 +254,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20240327155427-868f304927ed h1:n8QtJTrwsv3P7dNxPaMeNkMcxvUpqocsHLr8iDLGlQI=
github.com/google/pprof v0.0.0-20240327155427-868f304927ed/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -246,8 +265,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -260,15 +279,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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 v1.15.6 h1:WMYJbw2Wo+KOWwZFvgY0jMoVHM6i4XIvRs2RcBj5VmI=
github.com/jhump/protoreflect v1.15.6/go.mod h1:jCHoyYQIJnaabEYnbGwyo9hUqfyUMTbJw/tAut5t97E=
github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@@ -279,24 +298,52 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww=
github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok=
github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
@@ -309,44 +356,52 @@ github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDj
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs=
github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
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/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=
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
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/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo=
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
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/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
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/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w=
github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
@@ -361,57 +416,74 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/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=
go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw=
go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg=
go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ=
go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k=
go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
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/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
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.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
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.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
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/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -422,12 +494,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
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-20240222234643-814bf88cf225 h1:BzKNaIRXh1bD+1557OcFIHlpYBiVbK4zEyn8zBHi1SE=
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 h1:VI4qDpTkfFaCXEPrbojidLgVQhj2x4nzTccG0hjaLlU=
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -451,8 +523,10 @@ 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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -484,8 +558,11 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -495,6 +572,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -505,8 +584,10 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -544,28 +625,41 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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=
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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -615,8 +709,10 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
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=
@@ -683,10 +779,10 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa h1:Jt1XW5PaLXF1/ePZrznsh/aAUvI7Adfc3LY1dAKlzRs=
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:K4kfzHtI0kqWA79gecJarFtDn/Mls+GxQcg3Zox91Ac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -703,10 +799,10 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -717,9 +813,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -740,6 +835,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

241
service/internal/acl/acl.go Normal file
View File

@@ -0,0 +1,241 @@
package acl
import (
"context"
config "github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
"google.golang.org/grpc/metadata"
)
type PermissionBits int
const (
View PermissionBits = 1 << iota
Exec
Logs
)
func (p PermissionBits) Has(permission PermissionBits) bool {
return p&permission != 0
}
// User respresents a person.
type AuthenticatedUser struct {
Username string
Usergroup string
Provider string
SID string
Acls []string
}
func (u *AuthenticatedUser) IsGuest() bool {
return u.Username == "guest" && u.Provider == "system"
}
func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
if cfg.LogDebugOptions.AclNotMatched {
log.WithFields(log.Fields{
"User": user.Username,
"Action": action.Title,
}).Debugf("%v - No ACLs Matched", aclFunction)
}
}
func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
if cfg.LogDebugOptions.AclMatched {
log.WithFields(log.Fields{
"User": user.Username,
"Action": action.Title,
"ACL": acl.Name,
}).Debugf("%v - Matched ACL", aclFunction)
}
}
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, defaultPermission bool) {
if cfg.LogDebugOptions.AclNoneMatched {
log.WithFields(log.Fields{
"User": user.Username,
"Action": action.Title,
"Default": defaultPermission,
}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
}
}
func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits {
var ret PermissionBits
if permissions.View {
ret |= View
}
if permissions.Exec {
ret |= Exec
}
if permissions.Logs {
ret |= Logs
}
return ret
}
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action) bool {
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
if cfg.LogDebugOptions.AclCheckStarted {
log.WithFields(log.Fields{
"actionTitle": action.Title,
"username": user.Username,
"usergroup": user.Usergroup,
"relevantAcls": len(relevantAcls),
"requiredPermission": requiredPermission,
}).Debugf("ACL check - %v", aclFunction)
}
for _, acl := range relevantAcls {
permissionBits := permissionsConfigToBits(acl.Permissions)
if permissionBits.Has(requiredPermission) {
logAclMatched(cfg, aclFunction, user, action, acl)
return true
} else {
logAclNotMatched(cfg, aclFunction, user, action, acl)
}
}
logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs)
return defaultValue
}
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
func IsAllowedLogs(cfg *config.Config, user *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 {
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 {
if action.Hidden {
return false
}
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
}
func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
mdValues := md.Get(key)
if len(mdValues) > 0 {
return mdValues[0]
}
return ""
}
// UserFromContext tries to find a user from a grpc context
func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
var ret *AuthenticatedUser
md, ok := metadata.FromIncomingContext(ctx)
if ok {
ret = &AuthenticatedUser{}
ret.Username = getMetadataKeyOrEmpty(md, "username")
ret.Usergroup = getMetadataKeyOrEmpty(md, "usergroup")
ret.Provider = getMetadataKeyOrEmpty(md, "provider")
buildUserAcls(cfg, ret)
}
if !ok || ret.Username == "" {
ret = UserGuest(cfg)
}
log.WithFields(log.Fields{
"username": ret.Username,
"usergroup": ret.Usergroup,
"provider": ret.Provider,
"acls": ret.Acls,
}).Debugf("UserFromContext")
return ret
}
func UserGuest(cfg *config.Config) *AuthenticatedUser {
ret := &AuthenticatedUser{}
ret.Username = "guest"
ret.Usergroup = "guest"
ret.Provider = "system"
buildUserAcls(cfg, ret)
return ret
}
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
ret := &AuthenticatedUser{
Username: username,
Usergroup: "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 slices.Contains(acl.MatchUsergroups, user.Usergroup) {
user.Acls = append(user.Acls, acl.Name)
continue
}
}
}
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
if !slices.Contains(user.Acls, acl.Name) {
// If the user does not have this ACL, then it is not relevant
return false
}
if acl.AddToEveryAction {
return true
}
if slices.Contains(actionAcls, acl.Name) {
return true
}
return false
}
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *AuthenticatedUser) []*config.AccessControlList {
var ret []*config.AccessControlList
for _, acl := range cfg.AccessControlLists {
if isACLRelevantToAction(cfg, actionAcls, acl, user) {
ret = append(ret, acl)
}
}
return ret
}

View File

@@ -1,5 +1,9 @@
package config
import (
"fmt"
)
// Action represents the core functionality of OliveTin - commands that show up
// as buttons in the UI.
type Action struct {
@@ -17,7 +21,7 @@ type Action struct {
ExecOnFileCreatedInDir []string
ExecOnFileChangedInDir []string
ExecOnCalendarFile string
Trigger string
Triggers []string
MaxConcurrent int
MaxRate []RateSpec
Arguments []ActionArgument
@@ -83,6 +87,7 @@ type PrometheusConfig struct {
type Config struct {
UseSingleHTTPFrontend bool
ThemeName string
ThemeCacheDisabled bool
ListenAddressSingleHTTPFrontend string
ListenAddressWebUI string
ListenAddressRestActions string
@@ -91,6 +96,7 @@ type Config struct {
ExternalRestAddress string
LogLevel string
LogDebugOptions LogDebugOptions
LogHistoryPageSize int64
Actions []*Action `mapstructure:"actions"`
Entities []*EntityFile `mapstructure:"entities"`
Dashboards []*DashboardComponent `mapstructure:"dashboards"`
@@ -99,6 +105,7 @@ type Config struct {
ShowFooter bool
ShowNavigation bool
ShowNewVersions bool
EnableCustomJs bool
AuthJwtCookieName string
AuthJwtAud string
AuthJwtDomain string
@@ -109,6 +116,11 @@ type Config struct {
AuthJwtPubKeyPath string // will read pub key from file on disk
AuthHttpHeaderUsername string
AuthHttpHeaderUserGroup string
AuthLocalUsers AuthLocalUsersConfig
AuthLoginUrl string
AuthRequireGuestsToLogin bool
AuthOAuth2RedirectURL string
AuthOAuth2Providers map[string]*OAuth2Provider
DefaultPermissions PermissionsList
AccessControlLists []*AccessControlList
WebUIDir string
@@ -124,10 +136,45 @@ type Config struct {
DefaultIconForActions string
DefaultIconForDirectories string
DefaultIconForBack string
AdditionalNavigationLinks []*NavigationLink
usedConfigDir string
}
type AuthLocalUsersConfig struct {
Enabled bool
Users []*LocalUser
}
type LocalUser struct {
Username string
Usergroup string
Password string
}
type OAuth2Provider struct {
Name string
Title string
ClientID string
ClientSecret string
Icon string
Scopes []string
AuthUrl string
TokenUrl string
WhoamiUrl string
UsernameField string
UserGroupField string
InsecureSkipVerify bool
CallbackTimeout int
CertBundlePath string
}
type NavigationLink struct {
Title string
Url string
Target string
}
type SaveLogsConfig struct {
ResultsDirectory string
OutputDirectory string
@@ -136,8 +183,10 @@ type SaveLogsConfig struct {
type LogDebugOptions struct {
SingleFrontendRequests bool
SingleFrontendRequestHeaders bool
AclCheckStarted bool
AclMatched bool
AclNotMatched bool
AclNoneMatched bool
}
type DashboardComponent struct {
@@ -145,30 +194,33 @@ type DashboardComponent struct {
Type string
Entity string
Icon string
CssClass string
Contents []DashboardComponent
}
// DefaultConfig gets a new Config structure with sensible default values.
func DefaultConfig() *Config {
return DefaultConfigWithBasePort(1337)
}
// DefaultConfig gets a new Config structure with sensible default values.
func DefaultConfigWithBasePort(basePort int) *Config {
config := Config{}
config.UseSingleHTTPFrontend = true
config.PageTitle = "OliveTin"
config.ShowFooter = true
config.ShowNavigation = true
config.ShowNewVersions = true
config.ListenAddressSingleHTTPFrontend = "0.0.0.0:1337"
config.ListenAddressRestActions = "localhost:1338"
config.ListenAddressGrpcActions = "localhost:1339"
config.ListenAddressWebUI = "localhost:1340"
config.ListenAddressPrometheus = "localhost:1341"
config.EnableCustomJs = false
config.ExternalRestAddress = "."
config.LogLevel = "INFO"
config.CheckForUpdates = true
config.LogHistoryPageSize = 10
config.CheckForUpdates = false
config.DefaultPermissions.Exec = true
config.DefaultPermissions.View = true
config.DefaultPermissions.Logs = true
config.AuthJwtClaimUsername = "name"
config.AuthJwtClaimUserGroup = "group"
config.AuthRequireGuestsToLogin = false
config.WebUIDir = "./webui"
config.CronSupportForSeconds = false
config.SectionNavigationStyle = "sidebar"
@@ -182,6 +234,13 @@ func DefaultConfig() *Config {
config.DefaultIconForActions = "&#x1F600;"
config.DefaultIconForDirectories = "&#128193"
config.DefaultIconForBack = "&laquo;"
config.ThemeCacheDisabled = false
config.ListenAddressSingleHTTPFrontend = fmt.Sprintf("0.0.0.0:%d", basePort)
config.ListenAddressRestActions = fmt.Sprintf("localhost:%d", basePort+1)
config.ListenAddressGrpcActions = fmt.Sprintf("localhost:%d", basePort+2)
config.ListenAddressWebUI = fmt.Sprintf("localhost:%d", basePort+3)
config.ListenAddressPrometheus = fmt.Sprintf("localhost:%d", basePort+4)
return &config
}

View File

@@ -43,6 +43,16 @@ func (cfg *Config) FindAcl(aclTitle string) *AccessControlList {
return nil
}
func (cfg *Config) FindUserByUsername(searchUsername string) *LocalUser {
for _, user := range cfg.AuthLocalUsers.Users {
if user.Username == searchUsername {
return user
}
}
return nil
}
func (cfg *Config) SetDir(dir string) {
cfg.usedConfigDir = dir
}

View File

@@ -44,3 +44,10 @@ func TestFindAcl(t *testing.T) {
assert.NotNil(t, c.FindAcl("Testing ACL"), "Find a ACL that should exist")
assert.Nil(t, c.FindAcl("Chocolate Cake"), "Find a ACL that does not exist")
}
func TestSetDir(t *testing.T) {
c := DefaultConfig()
c.SetDir("test")
assert.Equal(t, "test", c.GetDir(), "SetDir")
}

View File

@@ -4,9 +4,10 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
log "github.com/sirupsen/logrus"
"os"
"path"
"path/filepath"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
@@ -14,7 +15,7 @@ import (
var (
metricConfigActionCount = promauto.NewGauge(prometheus.GaugeOpts{
Name: "olivetin_config_action_count",
Help: "Then number of actions in the config file",
Help: "The number of actions in the config file",
})
metricConfigReloadedCount = promauto.NewCounter(prometheus.CounterOpts{
@@ -38,7 +39,7 @@ func Reload(cfg *Config) {
metricConfigReloadedCount.Inc()
metricConfigActionCount.Set(float64(len(cfg.Actions)))
cfg.SetDir(path.Dir(viper.ConfigFileUsed()))
cfg.SetDir(filepath.Dir(viper.ConfigFileUsed()))
cfg.Sanitize()
for _, l := range listeners {

View File

@@ -14,6 +14,8 @@ var emojis = map[string]string{
"logs": "&#128269;",
"light": "&#128161;",
"robot": "&#129302;",
"ssh": "&#128272;",
"theme": "&#127912;",
}
func lookupHTMLIcon(keyToLookup string, defaultIcon string) string {

View File

@@ -41,6 +41,28 @@ func (action *Action) sanitize(cfg *Config) {
for idx := range action.Arguments {
action.Arguments[idx].sanitize()
}
sanitizeAuthRequireGuestsToLogin(cfg)
sanitizeLogHistoryPageSize(cfg)
}
func sanitizeAuthRequireGuestsToLogin(cfg *Config) {
if cfg.AuthRequireGuestsToLogin {
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
cfg.DefaultPermissions.View = false
cfg.DefaultPermissions.Exec = false
cfg.DefaultPermissions.Logs = false
}
}
func sanitizeLogHistoryPageSize(cfg *Config) {
if cfg.LogHistoryPageSize < 10 {
log.Warnf("LogsHistoryLimit is too low, setting it to 10")
cfg.LogHistoryPageSize = 10
} else if cfg.LogHistoryPageSize > 100 {
log.Warnf("LogsHistoryLimit is high, you can do this, but expect browser lag.")
}
}
func getActionID(action *Action) string {
@@ -55,10 +77,13 @@ func getActionID(action *Action) string {
return action.ID
}
//gocyclo:ignore
func sanitizePopupOnStart(raw string, cfg *Config) string {
switch raw {
case "execution-dialog":
return raw
case "execution-dialog-output-html":
return raw
case "execution-dialog-stdout-only":
return raw
case "execution-button":

View File

@@ -32,7 +32,8 @@ func SetupEntityFileWatchers(cfg *config.Config) {
configDir = configDirVar
}
for _, ef := range cfg.Entities {
for entityIndex, _ := range cfg.Entities { // #337 - iterate by key, not by value
ef := cfg.Entities[entityIndex]
p := ef.File
if !filepath.IsAbs(p) {
@@ -72,12 +73,12 @@ func loadEntityFileJson(filename string, entityname string) {
return
}
data := make([]map[string]string, 0)
data := make([]map[string]interface{}, 0)
decoder := json.NewDecoder(bytes.NewReader(jfile))
for decoder.More() {
d := make(map[string]string)
d := make(map[string]interface{})
err := decoder.Decode(&d)
@@ -105,7 +106,7 @@ func loadEntityFileYaml(filename string, entityname string) {
return
}
data := make([]map[string]string, 1)
data := make([]map[string]interface{}, 1)
err = yaml.Unmarshal(yfile, &data)
@@ -116,7 +117,7 @@ func loadEntityFileYaml(filename string, entityname string) {
updateSvFromFile(entityname, data)
}
func updateSvFromFile(entityname string, data []map[string]string) {
func updateSvFromFile(entityname string, data []map[string]interface{}) {
log.Debugf("updateSvFromFile: %+v", data)
count := len(data)
@@ -128,12 +129,34 @@ func updateSvFromFile(entityname string, data []map[string]string) {
for i, mapp := range data {
prefix := "entities." + entityname + "." + fmt.Sprintf("%v", i)
for k, v := range mapp {
sv.Set(prefix+"."+k, v)
}
serializeValueToSv(prefix, mapp)
}
for _, l := range listeners {
l()
}
}
func serializeValueToSv(prefix string, value interface{}) {
if m, ok := value.(map[string]interface{}); ok { // if value is a map we need to flatten it
serializeMapToSv(prefix, m)
} else if s, ok := value.([]interface{}); ok { // if value is a slice we need to flatten it
serializeSliceToSv(prefix, s)
} else {
sv.Set(prefix, fmt.Sprintf("%v", value))
}
}
func serializeMapToSv(prefix string, m map[string]interface{}) {
for k, v := range m {
serializeValueToSv(prefix+"."+k, v)
}
}
func serializeSliceToSv(prefix string, s []interface{}) {
sv.Set(prefix+".count", fmt.Sprintf("%v", len(s)))
for i, v := range s {
serializeValueToSv(prefix+"."+fmt.Sprintf("%v", i), v)
}
}

View File

@@ -6,6 +6,7 @@ import (
log "github.com/sirupsen/logrus"
"errors"
"net/mail"
"net/url"
"regexp"
"strings"
@@ -23,35 +24,51 @@ var (
}
)
func parseCommandForReplacements(rawShellCommand string, values map[string]string) (string, map[string]string, error) {
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
foundArgumentNames := r.FindAllStringSubmatch(rawShellCommand, -1)
usedArguments := make(map[string]string)
for _, match := range foundArgumentNames {
argName := match[1]
argValue, argProvided := values[argName]
if !argProvided {
return "", nil, errors.New("Required arg not provided: " + argName)
}
usedArguments[argName] = argValue
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
}
return rawShellCommand, usedArguments, nil
}
func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action, actionTitle string, entityPrefix string) (string, error) {
log.WithFields(log.Fields{
"actionTitle": actionTitle,
"cmd": rawShellCommand,
}).Infof("Action parse args - Before")
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
matches := r.FindAllStringSubmatch(rawShellCommand, -1)
rawShellCommand, usedArgs, err := parseCommandForReplacements(rawShellCommand, values)
for _, match := range matches {
argValue, argProvided := values[match[1]]
if err != nil {
return "", err
}
if !argProvided {
log.Infof("%v", values)
return "", errors.New("Required arg not provided: " + match[1])
}
err := typecheckActionArgument(match[1], argValue, action)
for argName, argValue := range usedArgs {
err := typecheckActionArgument(argName, argValue, action)
if err != nil {
return "", err
}
log.WithFields(log.Fields{
"name": match[1],
"name": argName,
"value": argValue,
}).Debugf("Arg assigned")
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
}
rawShellCommand = sv.ReplaceEntityVars(entityPrefix, rawShellCommand)
@@ -121,24 +138,47 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
// TypeSafetyCheck checks argument values match a specific type. The types are
// defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
// characters.
//
//gocyclo:ignore
func TypeSafetyCheck(name string, value string, argumentType string) error {
if argumentType == "url" {
return typeSafetyCheckUrl(name, value)
}
if argumentType == "datetime" {
_, err := time.Parse("2006-01-02T15:04:05", value)
if err != nil {
return err
}
switch argumentType {
case "password":
return nil
case "raw_string_multiline":
return nil
case "email":
return typeSafetyCheckEmail(name, value)
case "url":
return typeSafetyCheckUrl(name, value)
case "datetime":
return typeSafetyCheckDatetime(name, value)
}
return typeSafetyCheckRegex(name, value, argumentType)
}
func typeSafetyCheckEmail(name string, value string) error {
_, err := mail.ParseAddress(value)
log.Errorf("Email check: %v, %v", err, value)
if err != nil {
return err
}
return nil
}
func typeSafetyCheckDatetime(name string, value string) error {
_, err := time.Parse("2006-01-02T15:04:05", value)
if err != nil {
return err
}
return nil
}
func typeSafetyCheckRegex(name string, value string, argumentType string) error {
pattern := ""

View File

@@ -15,16 +15,14 @@ import (
"context"
"fmt"
"os"
"os/exec"
"path"
"runtime"
"strings"
"sync"
"time"
)
var (
metricActionsRequested = promauto.NewGauge(prometheus.GaugeOpts{
metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
Name: "olivetin_actions_requested_count",
Help: "The actions requested count",
})
@@ -39,8 +37,11 @@ type ActionBinding struct {
// Executor represents a helper class for executing commands. It's main method
// is ExecRequest
type Executor struct {
Logs map[string]*InternalLogEntry
LogsByActionId map[string][]*InternalLogEntry
logs map[string]*InternalLogEntry
logsTrackingIdsByDate []string
LogsByActionId map[string][]*InternalLogEntry
logmutex sync.RWMutex
MapActionIdToBinding map[string]*ActionBinding
MapActionIdToBindingLock sync.RWMutex
@@ -84,6 +85,8 @@ type InternalLogEntry struct {
ExecutionFinished bool
ExecutionTrackingID string
Process *os.Process
Username string
Index int64
/*
The following 3 properties are obviously on Action normally, but it's useful
@@ -102,7 +105,8 @@ type executorStepFunc func(*ExecutionRequest) bool
func DefaultExecutor(cfg *config.Config) *Executor {
e := Executor{}
e.Cfg = cfg
e.Logs = make(map[string]*InternalLogEntry)
e.logs = make(map[string]*InternalLogEntry)
e.logsTrackingIdsByDate = make([]string, 0)
e.LogsByActionId = make(map[string][]*InternalLogEntry)
e.MapActionIdToBinding = make(map[string]*ActionBinding)
@@ -124,7 +128,7 @@ func DefaultExecutor(cfg *config.Config) *Executor {
}
type listener interface {
OnExecutionStarted(actionTitle string)
OnExecutionStarted(logEntry *InternalLogEntry)
OnExecutionFinished(logEntry *InternalLogEntry)
OnOutputChunk(o []byte, executionTrackingId string)
OnActionMapRebuilt()
@@ -134,14 +138,106 @@ func (e *Executor) AddListener(m listener) {
e.listeners = append(e.listeners, m)
}
// getPagingStartIndex calculates the starting index for log pagination.
// Parameters:
//
// startOffset: The offset from the most recent log (0 means start from the most recent)
// totalLogCount: Total number of logs available
// count: Number of logs to retrieve
//
// Returns: The calculated starting index for pagination
func getPagingStartIndex(startOffset int64, totalLogCount int64, count int64) int64 {
var startIndex int64
if startOffset <= 0 {
startIndex = totalLogCount
} else {
startIndex = (totalLogCount - startOffset)
if startIndex < 0 {
startIndex = 1
}
}
return startIndex - 1
}
func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*InternalLogEntry, int64) {
e.logmutex.RLock()
totalLogCount := int64(len(e.logsTrackingIdsByDate))
startIndex := getPagingStartIndex(startOffset, totalLogCount, pageCount)
pageCount = min(totalLogCount, pageCount)
endIndex := max(0, (startIndex-pageCount)+1)
log.WithFields(log.Fields{
"startOffset": startOffset,
"pageCount": pageCount,
"total": totalLogCount,
"startIndex": startIndex,
"endIndex": endIndex,
}).Tracef("GetLogTrackingIds")
trackingIds := make([]*InternalLogEntry, 0, pageCount)
if totalLogCount > 0 {
for i := endIndex; i <= startIndex; i++ {
trackingIds = append(trackingIds, e.logs[e.logsTrackingIdsByDate[i]])
}
}
e.logmutex.RUnlock()
remainingLogs := endIndex
return trackingIds, remainingLogs
}
func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
e.logmutex.RLock()
entry, found := e.logs[trackingID]
e.logmutex.RUnlock()
return entry, found
}
func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
e.logmutex.RLock()
logs, found := e.LogsByActionId[actionId]
e.logmutex.RUnlock()
if !found {
return make([]*InternalLogEntry, 0)
}
return logs
}
func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
e.logmutex.Lock()
entry.Index = int64(len(e.logsTrackingIdsByDate))
e.logs[trackingID] = entry
e.logsTrackingIdsByDate = append(e.logsTrackingIdsByDate, trackingID)
e.logmutex.Unlock()
}
// ExecRequest processes an ExecutionRequest
func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
if req.AuthenticatedUser == nil {
req.AuthenticatedUser = acl.UserGuest(req.Cfg)
}
req.executor = e
// req.UUID is now set by the client, so that they can track the request
// from start to finish. This means that a malicious client could send
// duplicate UUIDs (or just random strings), but this is the only way.
req.logEntry = &InternalLogEntry{
DatetimeStarted: time.Now(),
ExecutionTrackingID: req.TrackingID,
@@ -152,15 +248,18 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string)
ActionId: "",
ActionTitle: "notfound",
ActionIcon: "&#x1f4a9;",
Username: req.AuthenticatedUser.Username,
}
_, foundLog := e.Logs[req.TrackingID]
_, isDuplicate := e.GetLog(req.TrackingID)
if foundLog || req.TrackingID == "" {
if isDuplicate || req.TrackingID == "" {
req.TrackingID = uuid.NewString()
}
e.Logs[req.TrackingID] = req.logEntry
log.Tracef("executor.ExecRequest(): %v", req)
e.SetLog(req.TrackingID, req.logEntry)
wg := new(sync.WaitGroup)
wg.Add(1)
@@ -184,18 +283,22 @@ func (e *Executor) execChain(req *ExecutionRequest) {
// This isn't a step, because we want to notify all listeners, irrespective
// of how many steps were actually executed.
notifyListeners(req)
notifyListenersFinished(req)
}
func getConcurrentCount(req *ExecutionRequest) int {
concurrentCount := 0
for _, log := range req.executor.LogsByActionId[req.Action.ID] {
req.executor.logmutex.RLock()
for _, log := range req.executor.GetLogsByActionId(req.Action.ID) {
if !log.ExecutionFinished {
concurrentCount += 1
}
}
req.executor.logmutex.RUnlock()
return concurrentCount
}
@@ -237,7 +340,7 @@ func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
then := time.Now().Add(-duration)
for _, logEntry := range req.executor.LogsByActionId[req.Action.ID] {
for _, logEntry := range req.executor.GetLogsByActionId(req.Action.ID) {
if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked {
executions += 1
@@ -268,12 +371,30 @@ func stepRateCheck(req *ExecutionRequest) bool {
}
func stepACLCheck(req *ExecutionRequest) bool {
return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
canExec := acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
if !canExec {
req.logEntry.Output = "ACL check failed. Blocked from executing."
req.logEntry.Blocked = true
log.WithFields(log.Fields{
"actionTitle": req.logEntry.ActionTitle,
}).Warnf("ACL check failed. Blocked from executing.")
}
return canExec
}
func stepParseArgs(req *ExecutionRequest) bool {
var err error
if req.Arguments == nil {
req.Arguments = make(map[string]string)
}
req.Arguments["ot_executionTrackingId"] = req.TrackingID
req.Arguments["ot_username"] = req.AuthenticatedUser.Username
req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
if err != nil {
@@ -312,6 +433,9 @@ func stepRequestAction(req *ExecutionRequest) bool {
req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
req.logEntry.ActionIcon = req.Action.Icon
req.logEntry.ActionId = req.Action.ID
req.logEntry.Tags = req.Tags
req.executor.logmutex.Lock()
if _, containsKey := req.executor.LogsByActionId[req.Action.ID]; !containsKey {
req.executor.LogsByActionId[req.Action.ID] = make([]*InternalLogEntry, 0)
@@ -319,11 +443,15 @@ func stepRequestAction(req *ExecutionRequest) bool {
req.executor.LogsByActionId[req.Action.ID] = append(req.executor.LogsByActionId[req.Action.ID], req.logEntry)
req.executor.logmutex.Unlock()
log.WithFields(log.Fields{
"actionTitle": req.logEntry.ActionTitle,
"tags": req.Tags,
}).Infof("Action requested")
notifyListenersStarted(req)
return true
}
@@ -331,7 +459,7 @@ func stepLogStart(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"actionTitle": req.logEntry.ActionTitle,
"timeout": req.Action.Timeout,
}).Infof("Action starting")
}).Infof("Action started")
return true
}
@@ -349,18 +477,16 @@ func stepLogFinish(req *ExecutionRequest) bool {
return true
}
func notifyListeners(req *ExecutionRequest) {
func notifyListenersFinished(req *ExecutionRequest) {
for _, listener := range req.executor.listeners {
listener.OnExecutionFinished(req.logEntry)
}
}
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
if runtime.GOOS == "windows" {
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
func notifyListenersStarted(req *ExecutionRequest) {
for _, listener := range req.executor.listeners {
listener.OnExecutionStarted(req.logEntry)
}
return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
}
func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
@@ -386,11 +512,18 @@ func (ost *OutputStreamer) String() string {
return ost.output.String()
}
func buildEnv(req *ExecutionRequest) []string {
func buildEnv(args map[string]string) []string {
ret := append(os.Environ(), "OLIVETIN=1")
for k, v := range req.Arguments {
ret = append(ret, fmt.Sprintf("%v=%v", strings.ToUpper(k), v))
for k, v := range args {
varName := fmt.Sprintf("%v", strings.TrimSpace(strings.ToUpper(k)))
// Skip arguments that might not have a name (eg, confirmation), as this causes weird bugs on Windows.
if varName == "" {
continue
}
ret = append(ret, fmt.Sprintf("%v=%v", varName, v))
}
return ret
@@ -405,7 +538,7 @@ func stepExec(req *ExecutionRequest) bool {
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
cmd.Stdout = streamer
cmd.Stderr = streamer
cmd.Env = buildEnv(req)
cmd.Env = buildEnv(req.Arguments)
req.logEntry.ExecutionStarted = true
@@ -422,10 +555,16 @@ func stepExec(req *ExecutionRequest) bool {
appendErrorToStderr(waiterr, req.logEntry)
if ctx.Err() == context.DeadlineExceeded {
log.WithFields(log.Fields{
"actionTitle": req.logEntry.ActionTitle,
}).Warnf("Action timed out")
// The context timeout should kill the process, but let's make sure.
req.executor.Kill(req.logEntry)
req.logEntry.TimedOut = true
req.logEntry.Output += "OliveTin::timeout - this action timed out after " + fmt.Sprintf("%v", req.Action.Timeout) + " seconds. If you need more time for this action, set a longer timeout. See https://docs.olivetin.app/timeout.html for more help."
}
req.logEntry.Tags = req.Tags
req.logEntry.DatetimeFinished = time.Now()
return true
@@ -443,39 +582,71 @@ func stepExecAfter(req *ExecutionRequest) bool {
var stderr bytes.Buffer
args := map[string]string{
"output": req.logEntry.Output,
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
"output": req.logEntry.Output,
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
"ot_executionTrackingId": req.TrackingID,
"ot_username": req.AuthenticatedUser.Username,
}
finalParsedCommand, _ := parseActionArguments(req.Action.ShellAfterCompleted, args, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
finalParsedCommand, _, err := parseCommandForReplacements(req.Action.ShellAfterCompleted, args)
if err != nil {
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
req.logEntry.Output += msg
log.Warnf(msg)
return true
}
cmd := wrapCommandInShell(ctx, finalParsedCommand)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Env = buildEnv(args)
runerr := cmd.Start()
waiterr := cmd.Wait()
req.logEntry.Output += "---\n" + stdout.String()
req.logEntry.Output += "---\n" + stderr.String()
req.logEntry.Output += "\n"
req.logEntry.Output += "OliveTin::shellAfterCompleted stdout\n"
req.logEntry.Output += stdout.String()
req.logEntry.Output += "OliveTin::shellAfterCompleted stderr\n"
req.logEntry.Output += stderr.String()
req.logEntry.Output += "OliveTin::shellAfterCompleted errors and summary\n"
appendErrorToStderr(runerr, req.logEntry)
appendErrorToStderr(waiterr, req.logEntry)
if ctx.Err() == context.DeadlineExceeded {
req.logEntry.Output += "Your shellAfterCommand command timed out."
req.logEntry.Output += "Your shellAfterCompleted command timed out."
}
req.logEntry.Output += fmt.Sprintf("Your shellAfterCommand exited with code %v", cmd.ProcessState.ExitCode())
req.logEntry.Output += fmt.Sprintf("Your shellAfterCompleted exited with code %v\n", cmd.ProcessState.ExitCode())
req.logEntry.Output += "OliveTin::shellAfterCompleted output complete\n"
return true
}
func stepTrigger(req *ExecutionRequest) bool {
if req.Action.Trigger != "" {
if req.Action.Triggers == nil {
return true
}
if len(req.Tags) > 0 && req.Tags[0] == "trigger" {
log.Warnf("Trigger action is triggering another trigger action. This is allowed, but be careful not to create trigger loops.")
}
triggerLoop(req)
return true
}
func triggerLoop(req *ExecutionRequest) {
for _, triggerReq := range req.Action.Triggers {
trigger := &ExecutionRequest{
ActionTitle: req.Action.Trigger,
ActionTitle: triggerReq,
TrackingID: uuid.NewString(),
Tags: []string{"trigger"},
AuthenticatedUser: req.AuthenticatedUser,
@@ -484,8 +655,6 @@ func stepTrigger(req *ExecutionRequest) bool {
req.executor.ExecRequest(trigger)
}
return true
}
func stepSaveLog(req *ExecutionRequest) bool {

View File

@@ -109,3 +109,73 @@ func TestArgumentNameSnakeCase(t *testing.T) {
assert.Equal(t, "echo 'Tickling Fred'", out)
assert.Nil(t, err)
}
func TestGetLogsEmpty(t *testing.T) {
e, cfg := testingExecutor()
assert.Equal(t, int64(10), cfg.LogHistoryPageSize, "Logs page size should be 10")
logs, remaining := e.GetLogTrackingIds(0, 10)
assert.NotNil(t, logs, "Logs should not be nil")
assert.Equal(t, 0, len(logs), "No logs yet")
assert.Equal(t, int64(0), remaining, "There should be no remaining logs")
}
func TestGetLogsLessThanPageSize(t *testing.T) {
e, cfg := testingExecutor()
cfg.Actions = append(cfg.Actions, &config.Action{
Title: "blat",
Shell: "date",
})
assert.Equal(t, int64(10), cfg.LogHistoryPageSize, "Logs page size should be 10")
logEntries, remaining := e.GetLogTrackingIds(0, 10)
assert.Equal(t, 0, len(logEntries), "There should be 0 logs")
assert.Zero(t, remaining, "There should be no remaining logs")
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
logEntries, remaining = e.GetLogTrackingIds(0, 10)
assert.Equal(t, 7, len(logEntries), "There should be 7 logs")
assert.Zero(t, remaining, "There should be no remaining logs")
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
execNewReqAndWait(e, "blat", cfg)
logEntries, remaining = e.GetLogTrackingIds(0, 10)
assert.Equal(t, 10, len(logEntries), "There should be 10 logs")
assert.Equal(t, int64(2), remaining, "There should be 1 remaining logs")
}
func execNewReqAndWait(e *Executor, title string, cfg *config.Config) {
req := &ExecutionRequest{
ActionTitle: title,
Cfg: cfg,
}
wg, _ := e.ExecRequest(req)
wg.Wait()
}
func TestGetPagingIndexes(t *testing.T) {
assert.Zero(t, getPagingStartIndex(5, 0, 5), "Testing start index from empty list")
assert.Equal(t, int64(4), getPagingStartIndex(5, 10, 5), "Testing start index from mid point")
assert.Equal(t, int64(9), getPagingStartIndex(-1, 10, 5), "Testing start index with negative offset")
assert.Equal(t, int64(0), getPagingStartIndex(15, 10, 5), "Testing start index with large offset")
assert.Equal(t, int64(9), getPagingStartIndex(0, 10, 0), "Testing start index with zero count")
}

View File

@@ -0,0 +1,25 @@
//go:build !windows
// +build !windows
package executor
import (
"context"
"os/exec"
"syscall"
)
func (e *Executor) Kill(execReq *InternalLogEntry) error {
// A negative PID means to kill the whole process group. This is *nix specific behavior.
return syscall.Kill(-execReq.Process.Pid, syscall.SIGKILL)
}
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
// This is to ensure that the process group is killed when the parent process is killed.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return cmd
}

View File

@@ -0,0 +1,17 @@
//go:build windows
// +build windows
package executor
import (
"context"
"os/exec"
)
func (e *Executor) Kill(execReq *InternalLogEntry) error {
return execReq.Process.Kill()
}
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
}

View File

@@ -148,7 +148,7 @@ func processDebounce(ctx *watchContext) {
debounceWriteLog[ctx.filename] = logEntry
}
log.Infof("fsnotify event %+v", logEntry)
log.Debugf("fsnotify event %+v", logEntry)
if logEntry.callbackComplete || logEntry.callbackWrapper == nil {
log.Debugf("fsnotify event callback queued within debounce delay: %v", ctx.filename)

View File

@@ -0,0 +1,38 @@
package filehelper
import (
log "github.com/sirupsen/logrus"
"os"
"sync"
)
var writeFileMutex sync.Mutex
func WriteFile(filename string, out []byte) {
writeFileMutex.Lock()
defer writeFileMutex.Unlock()
if _, err := os.Stat(filename); os.IsNotExist(err) {
handle, err := os.Create(filename)
handle.Close()
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Errorf("Failed to create %v", filename)
return
}
}
err := os.WriteFile(filename, out, 0600)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Errorf("Failed to write session to %v", filename)
return
}
}

View File

@@ -2,15 +2,18 @@ package grpcapi
import (
ctx "context"
pb "github.com/OliveTin/OliveTin/gen/grpc"
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"errors"
"net"
"sort"
acl "github.com/OliveTin/OliveTin/internal/acl"
config "github.com/OliveTin/OliveTin/internal/config"
@@ -25,34 +28,40 @@ var (
type oliveTinAPI struct {
// Uncomment this if you want to allow undefined methods during dev.
// pb.UnimplementedOliveTinApiServiceServer
// apiv1.UnimplementedOliveTinApiServiceServer
executor *executor.Executor
}
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *pb.KillActionRequest) (*pb.KillActionResponse, error) {
ret := &pb.KillActionResponse{
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *apiv1.KillActionRequest) (*apiv1.KillActionResponse, error) {
ret := &apiv1.KillActionResponse{
ExecutionTrackingId: req.ExecutionTrackingId,
}
execReq, found := api.executor.Logs[req.ExecutionTrackingId]
execReqLogEntry, found := api.executor.GetLog(req.ExecutionTrackingId)
ret.Found = found
if found {
err := execReq.Process.Kill()
log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId)
if err == nil {
err := api.executor.Kill(execReqLogEntry)
if err != nil {
log.Warnf("Killing execution request err: %v", err)
ret.AlreadyCompleted = true
ret.Killed = false
} else {
ret.Killed = true
}
} else {
log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
}
return ret, nil
}
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *apiv1.StartActionRequest) (*apiv1.StartActionResponse, error) {
args := make(map[string]string)
for _, arg := range req.Arguments {
@@ -63,25 +72,70 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
pair := api.executor.MapActionIdToBinding[req.ActionId]
api.executor.MapActionIdToBindingLock.RUnlock()
if pair == nil || pair.Action == nil {
return nil, status.Errorf(codes.NotFound, "Action not found.")
}
authenticatedUser := acl.UserFromContext(ctx, cfg)
execReq := executor.ExecutionRequest{
Action: pair.Action,
EntityPrefix: pair.EntityPrefix,
TrackingID: req.UniqueTrackingId,
Arguments: args,
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
AuthenticatedUser: authenticatedUser,
Cfg: cfg,
}
api.executor.ExecRequest(&execReq)
return &pb.StartActionResponse{
return &apiv1.StartActionResponse{
ExecutionTrackingId: execReq.TrackingID,
}, nil
}
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionAndWaitRequest) (*pb.StartActionAndWaitResponse, error) {
func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *apiv1.PasswordHashRequest) (*httpbody.HttpBody, error) {
hash, err := createHash(req.Password)
if err != nil {
return nil, status.Errorf(codes.Internal, "Error creating hash.")
}
ret := &httpbody.HttpBody{
ContentType: "text/plain",
Data: []byte("Your password hash is: " + hash),
}
return ret, nil
}
func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *apiv1.LocalUserLoginRequest) (*apiv1.LocalUserLoginResponse, error) {
match := checkUserPassword(cfg, req.Username, req.Password)
if match {
grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
log.WithFields(log.Fields{
"username": req.Username,
}).Info("LocalUserLogin: User logged in successfully.")
} else {
log.WithFields(log.Fields{
"username": req.Username,
}).Warn("LocalUserLogin: User login failed.")
}
return &apiv1.LocalUserLoginResponse{
Success: match,
}, nil
}
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActionAndWaitRequest) (*apiv1.StartActionAndWaitResponse, error) {
args := make(map[string]string)
for _, arg := range req.Arguments {
args[arg.Name] = arg.Value
}
execReq := executor.ExecutionRequest{
Action: api.executor.FindActionBindingByID(req.ActionId),
TrackingID: uuid.NewString(),
@@ -93,10 +147,10 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionA
wg, _ := api.executor.ExecRequest(&execReq)
wg.Wait()
internalLogEntry, ok := api.executor.Logs[execReq.TrackingID]
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
if ok {
return &pb.StartActionAndWaitResponse{
return &apiv1.StartActionAndWaitResponse{
LogEntry: internalLogEntryToPb(internalLogEntry),
}, nil
} else {
@@ -104,7 +158,7 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionA
}
}
func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *pb.StartActionByGetRequest) (*pb.StartActionByGetResponse, error) {
func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *apiv1.StartActionByGetRequest) (*apiv1.StartActionByGetResponse, error) {
args := make(map[string]string)
execReq := executor.ExecutionRequest{
@@ -117,12 +171,12 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *pb.StartActionByG
_, uniqueTrackingId := api.executor.ExecRequest(&execReq)
return &pb.StartActionByGetResponse{
return &apiv1.StartActionByGetResponse{
ExecutionTrackingId: uniqueTrackingId,
}, nil
}
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *pb.StartActionByGetAndWaitRequest) (*pb.StartActionByGetAndWaitResponse, error) {
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.StartActionByGetAndWaitRequest) (*apiv1.StartActionByGetAndWaitResponse, error) {
args := make(map[string]string)
execReq := executor.ExecutionRequest{
@@ -136,24 +190,25 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *pb.StartAc
wg, _ := api.executor.ExecRequest(&execReq)
wg.Wait()
internalLogEntry, ok := api.executor.Logs[execReq.TrackingID]
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
if ok {
return &pb.StartActionByGetAndWaitResponse{
return &apiv1.StartActionByGetAndWaitResponse{
LogEntry: internalLogEntryToPb(internalLogEntry),
}, nil
} else {
return nil, errors.New("Execution not found!")
return nil, status.Errorf(codes.NotFound, "Execution not found.")
}
}
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *pb.LogEntry {
return &pb.LogEntry{
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry {
return &apiv1.LogEntry{
ActionTitle: logEntry.ActionTitle,
ActionIcon: logEntry.ActionIcon,
ActionId: logEntry.ActionId,
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
DatetimeIndex: logEntry.Index,
Output: logEntry.Output,
TimedOut: logEntry.TimedOut,
Blocked: logEntry.Blocked,
@@ -162,11 +217,12 @@ func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *pb.LogEntry {
ExecutionTrackingId: logEntry.ExecutionTrackingID,
ExecutionStarted: logEntry.ExecutionStarted,
ExecutionFinished: logEntry.ExecutionFinished,
User: logEntry.Username,
}
}
func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
logEntry, ok := api.executor.Logs[executionTrackingId]
logEntry, ok := api.executor.GetLog(executionTrackingId)
if !ok {
return nil
@@ -178,33 +234,41 @@ func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string
func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
var ile *executor.InternalLogEntry
for _, candidateLe := range api.executor.Logs {
if actionId == candidateLe.ActionId {
ile = candidateLe
}
logs := api.executor.GetLogsByActionId(actionId)
if len(logs) == 0 {
return nil
} else {
// Get last log entry
ile = logs[len(logs)-1]
}
return ile
}
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
res := &pb.ExecutionStatusResponse{}
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionStatusRequest) (*apiv1.ExecutionStatusResponse, error) {
res := &apiv1.ExecutionStatusResponse{}
var ile *executor.InternalLogEntry
if req.ExecutionTrackingId != "" {
ile = getExecutionStatusByTrackingID(api, req.ExecutionTrackingId)
} else {
ile = getMostRecentExecutionStatusById(api, req.ActionId)
}
res.LogEntry = internalLogEntryToPb(ile)
if ile == nil {
return nil, status.Error(codes.NotFound, "Execution not found")
} else {
res.LogEntry = internalLogEntryToPb(ile)
}
return res, nil
}
/**
func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
func (api *oliveTinAPI) WatchExecution(req *apiv1.WatchExecutionRequest, srv apiv1.OliveTinApi_WatchExecutionServer) error {
log.Infof("Watch")
if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
@@ -220,7 +284,7 @@ func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.Oli
log.Infof("%v %v", red, err)
srv.Send(&pb.WatchExecutionUpdate{
srv.Send(&apiv1.WatchExecutionUpdate{
Update: string(tmp),
})
}
@@ -231,47 +295,60 @@ func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.Oli
}
*/
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
func (api *oliveTinAPI) Logout(ctx ctx.Context, req *apiv1.LogoutRequest) (*httpbody.HttpBody, error) {
user := acl.UserFromContext(ctx, cfg)
grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
return nil, nil
}
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *apiv1.GetDashboardComponentsRequest) (*apiv1.GetDashboardComponentsResponse, error) {
user := acl.UserFromContext(ctx, cfg)
if user.IsGuest() && cfg.AuthRequireGuestsToLogin {
return nil, status.Errorf(codes.PermissionDenied, "Guests are not allowed to access the dashboard.")
}
res := buildDashboardResponse(api.executor, cfg, user)
if len(res.Actions) == 0 {
log.Warn("Zero actions found - check that you have some actions defined, with a view permission")
log.WithFields(log.Fields{
"username": user.Username,
"usergroup": user.Usergroup,
"provider": user.Provider,
"acls": user.Acls,
"availableActions": len(cfg.Actions),
}).Warn("Zero actions found for user")
}
log.Tracef("GetDashboardComponents: %v", res)
dashboardCfgToPb(res, cfg.Dashboards, cfg)
res.AuthenticatedUser = user.Username
return res, nil
}
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.GetLogsResponse, error) {
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *apiv1.GetLogsRequest) (*apiv1.GetLogsResponse, error) {
user := acl.UserFromContext(ctx, cfg)
ret := &pb.GetLogsResponse{}
ret := &apiv1.GetLogsResponse{}
// TODO Limit to 10 entries or something to prevent browser lag.
logEntries, countRemaining := api.executor.GetLogTrackingIds(req.StartOffset, cfg.LogHistoryPageSize)
for trackingId, logEntry := range api.executor.Logs {
for _, logEntry := range logEntries {
action := cfg.FindAction(logEntry.ActionTitle)
if action == nil || acl.IsAllowedLogs(cfg, user, action) {
pbLogEntry := internalLogEntryToPb(logEntry)
pbLogEntry.ExecutionTrackingId = trackingId
ret.Logs = append(ret.Logs, pbLogEntry)
}
}
sorter := func(i, j int) bool {
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
}
sort.Slice(ret.Logs, sorter)
ret.CountRemaining = countRemaining
ret.PageSize = cfg.LogHistoryPageSize
return ret, nil
}
@@ -281,7 +358,7 @@ This function is ONLY a helper for the UI - the arguments are validated properly
on the StartAction -> Executor chain. This is here basically to provide helpful
error messages more quickly before starting the action.
*/
func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *pb.ValidateArgumentTypeRequest) (*pb.ValidateArgumentTypeResponse, error) {
func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *apiv1.ValidateArgumentTypeRequest) (*apiv1.ValidateArgumentTypeResponse, error) {
err := executor.TypeSafetyCheck("", req.Value, req.Type)
desc := ""
@@ -289,17 +366,21 @@ func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *pb.ValidateAr
desc = err.Error()
}
return &pb.ValidateArgumentTypeResponse{
return &apiv1.ValidateArgumentTypeResponse{
Valid: err == nil,
Description: desc,
}, nil
}
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *pb.WhoAmIRequest) (*pb.WhoAmIResponse, error) {
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *apiv1.WhoAmIRequest) (*apiv1.WhoAmIResponse, error) {
user := acl.UserFromContext(ctx, cfg)
res := &pb.WhoAmIResponse{
res := &apiv1.WhoAmIResponse{
AuthenticatedUser: user.Username,
Usergroup: user.Usergroup,
Provider: user.Provider,
Sid: user.SID,
Acls: user.Acls,
}
log.Warnf("usergroup: %v", user.Usergroup)
@@ -307,7 +388,7 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *pb.WhoAmIRequest) (*pb.WhoA
return res, nil
}
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *pb.SosReportRequest) (*httpbody.HttpBody, error) {
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *apiv1.SosReportRequest) (*httpbody.HttpBody, error) {
sos := installationinfo.GetSosReport()
if !cfg.InsecureAllowDumpSos {
@@ -323,8 +404,8 @@ func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *pb.SosReportRequest) (*h
return ret, nil
}
func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *pb.DumpVarsRequest) (*pb.DumpVarsResponse, error) {
res := &pb.DumpVarsResponse{}
func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *apiv1.DumpVarsRequest) (*apiv1.DumpVarsResponse, error) {
res := &apiv1.DumpVarsResponse{}
if !cfg.InsecureAllowDumpVars {
res.Alert = "Dumping variables is not allowed by default because it is insecure."
@@ -338,9 +419,9 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *pb.DumpVarsRequest) (*pb.
return res, nil
}
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *pb.DumpPublicIdActionMapRequest) (*pb.DumpPublicIdActionMapResponse, error) {
res := &pb.DumpPublicIdActionMapResponse{}
res.Contents = make(map[string]*pb.ActionEntityPair)
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *apiv1.DumpPublicIdActionMapRequest) (*apiv1.DumpPublicIdActionMapResponse, error) {
res := &apiv1.DumpPublicIdActionMapResponse{}
res.Contents = make(map[string]*apiv1.ActionEntityPair)
if !cfg.InsecureAllowDumpActionMap {
res.Alert = "Dumping Public IDs is disallowed."
@@ -351,7 +432,7 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *pb.DumpPubli
api.executor.MapActionIdToBindingLock.RLock()
for k, v := range api.executor.MapActionIdToBinding {
res.Contents[k] = &pb.ActionEntityPair{
res.Contents[k] = &apiv1.ActionEntityPair{
ActionTitle: v.Action.Title,
EntityPrefix: v.EntityPrefix,
}
@@ -364,8 +445,8 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *pb.DumpPubli
return res, nil
}
func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *pb.GetReadyzRequest) (*pb.GetReadyzResponse, error) {
res := &pb.GetReadyzResponse{
func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *apiv1.GetReadyzRequest) (*apiv1.GetReadyzResponse, error) {
res := &apiv1.GetReadyzResponse{
Status: "OK",
}
@@ -376,6 +457,10 @@ func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *pb.GetReadyzRequest) (*p
func Start(globalConfig *config.Config, ex *executor.Executor) {
cfg = globalConfig
log.WithFields(log.Fields{
"address": cfg.ListenAddressGrpcActions,
}).Info("Starting gRPC API")
lis, err := net.Listen("tcp", cfg.ListenAddressGrpcActions)
if err != nil {
@@ -383,7 +468,7 @@ func Start(globalConfig *config.Config, ex *executor.Executor) {
}
grpcServer := grpc.NewServer()
pb.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
apiv1.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
err = grpcServer.Serve(lis)

View File

@@ -1,7 +1,7 @@
package grpcapi
import (
pb "github.com/OliveTin/OliveTin/gen/grpc"
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
acl "github.com/OliveTin/OliveTin/internal/acl"
config "github.com/OliveTin/OliveTin/internal/config"
executor "github.com/OliveTin/OliveTin/internal/executor"
@@ -9,8 +9,11 @@ import (
"sort"
)
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *pb.GetDashboardComponentsResponse {
res := &pb.GetDashboardComponentsResponse{}
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *apiv1.GetDashboardComponentsResponse {
res := &apiv1.GetDashboardComponentsResponse{
AuthenticatedUser: user.Username,
AuthenticatedUserProvider: user.Provider,
}
ex.MapActionIdToBindingLock.RLock()
@@ -35,10 +38,10 @@ func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl
return res
}
func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *pb.Action {
func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *apiv1.Action {
action := actionBinding.Action
btn := pb.Action{
btn := apiv1.Action{
Id: actionId,
Title: sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Title),
Icon: action.Icon,
@@ -48,7 +51,7 @@ func buildAction(actionId string, actionBinding *executor.ActionBinding, user *a
}
for _, cfgArg := range action.Arguments {
pbArg := pb.ActionArgument{
pbArg := apiv1.ActionArgument{
Name: cfgArg.Name,
Title: cfgArg.Title,
Type: cfgArg.Type,
@@ -64,7 +67,7 @@ func buildAction(actionId string, actionBinding *executor.ActionBinding, user *a
return &btn
}
func buildChoices(arg config.ActionArgument) []*pb.ActionArgumentChoice {
func buildChoices(arg config.ActionArgument) []*apiv1.ActionArgumentChoice {
if arg.Entity != "" && len(arg.Choices) == 1 {
return buildChoicesEntity(arg.Choices[0], arg.Entity)
} else {
@@ -72,15 +75,15 @@ func buildChoices(arg config.ActionArgument) []*pb.ActionArgumentChoice {
}
}
func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle string) []*pb.ActionArgumentChoice {
ret := []*pb.ActionArgumentChoice{}
func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle string) []*apiv1.ActionArgumentChoice {
ret := []*apiv1.ActionArgumentChoice{}
entityCount := sv.GetEntityCount(entityTitle)
for i := 0; i < entityCount; i++ {
prefix := sv.GetEntityPrefix(entityTitle, i)
ret = append(ret, &pb.ActionArgumentChoice{
ret = append(ret, &apiv1.ActionArgumentChoice{
Value: sv.ReplaceEntityVars(prefix, firstChoice.Value),
Title: sv.ReplaceEntityVars(prefix, firstChoice.Title),
})
@@ -89,11 +92,11 @@ func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle str
return ret
}
func buildChoicesSimple(choices []config.ActionArgumentChoice) []*pb.ActionArgumentChoice {
ret := []*pb.ActionArgumentChoice{}
func buildChoicesSimple(choices []config.ActionArgumentChoice) []*apiv1.ActionArgumentChoice {
ret := []*apiv1.ActionArgumentChoice{}
for _, cfgChoice := range choices {
pbChoice := pb.ActionArgumentChoice{
pbChoice := apiv1.ActionArgumentChoice{
Value: cfgChoice.Value,
Title: cfgChoice.Title,
}

View File

@@ -1,14 +1,14 @@
package grpcapi
import (
pb "github.com/OliveTin/OliveTin/gen/grpc"
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
"golang.org/x/exp/slices"
)
func dashboardCfgToPb(res *pb.GetDashboardComponentsResponse, dashboards []*config.DashboardComponent, cfg *config.Config) {
func dashboardCfgToPb(res *apiv1.GetDashboardComponentsResponse, dashboards []*config.DashboardComponent, cfg *config.Config) {
for _, dashboard := range dashboards {
res.Dashboards = append(res.Dashboards, &pb.DashboardComponent{
res.Dashboards = append(res.Dashboards, &apiv1.DashboardComponent{
Type: "dashboard",
Title: dashboard.Title,
Contents: getDashboardComponentContents(dashboard, cfg),
@@ -16,8 +16,8 @@ func dashboardCfgToPb(res *pb.GetDashboardComponentsResponse, dashboards []*conf
}
}
func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *config.Config) []*pb.DashboardComponent {
ret := make([]*pb.DashboardComponent, 0)
func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *config.Config) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0)
for _, subitem := range dashboard.Contents {
if subitem.Type == "fieldset" && subitem.Entity != "" {
@@ -25,11 +25,12 @@ func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *co
continue
}
newitem := &pb.DashboardComponent{
newitem := &apiv1.DashboardComponent{
Title: subitem.Title,
Type: getDashboardComponentType(&subitem),
Contents: getDashboardComponentContents(&subitem, cfg),
Icon: getDashboardComponentIcon(&subitem, cfg),
CssClass: subitem.CssClass,
}
ret = append(ret, newitem)

View File

@@ -1,13 +1,13 @@
package grpcapi
import (
pb "github.com/OliveTin/OliveTin/gen/grpc"
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
)
func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []*pb.DashboardComponent {
ret := make([]*pb.DashboardComponent, 0)
func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0)
entityCount := sv.GetEntityCount(entityTitle)
@@ -18,21 +18,23 @@ func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []
return ret
}
func buildEntityFieldset(tpl *config.DashboardComponent, entityTitle string, entityIndex int) *pb.DashboardComponent {
func buildEntityFieldset(tpl *config.DashboardComponent, entityTitle string, entityIndex int) *apiv1.DashboardComponent {
prefix := sv.GetEntityPrefix(entityTitle, entityIndex)
return &pb.DashboardComponent{
return &apiv1.DashboardComponent{
Title: sv.ReplaceEntityVars(prefix, tpl.Title),
Type: "fieldset",
Contents: buildEntityFieldsetContents(tpl.Contents, prefix),
CssClass: sv.ReplaceEntityVars(prefix, tpl.CssClass),
}
}
func buildEntityFieldsetContents(contents []config.DashboardComponent, prefix string) []*pb.DashboardComponent {
ret := make([]*pb.DashboardComponent, 0)
func buildEntityFieldsetContents(contents []config.DashboardComponent, prefix string) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0)
for _, subitem := range contents {
clone := &pb.DashboardComponent{}
clone := &apiv1.DashboardComponent{}
clone.CssClass = sv.ReplaceEntityVars(prefix, subitem.CssClass)
if subitem.Type == "" || subitem.Type == "link" {
clone.Type = "link"

View File

@@ -12,7 +12,7 @@ import (
log "github.com/sirupsen/logrus"
pb "github.com/OliveTin/OliveTin/gen/grpc"
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/executor"
)
@@ -26,7 +26,7 @@ func initServer(cfg *config.Config) *executor.Executor {
lis = bufconn.Listen(bufSize)
s := grpc.NewServer()
pb.RegisterOliveTinApiServiceServer(s, newServer(ex))
apiv1.RegisterOliveTinApiServiceServer(s, newServer(ex))
go func() {
if err := s.Serve(lis); err != nil {
@@ -41,7 +41,7 @@ func bufDialer(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiServiceClient) {
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, apiv1.OliveTinApiServiceClient) {
cfg = injectedConfig
ctx := context.Background()
@@ -52,7 +52,7 @@ func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*gr
t.Fatalf("Failed to dial bufnet: %v", err)
}
client := pb.NewOliveTinApiServiceClient(conn)
client := apiv1.NewOliveTinApiServiceClient(conn)
return conn, client
}
@@ -72,7 +72,7 @@ func TestGetActionsAndStart(t *testing.T) {
conn, client := getNewTestServerAndClient(t, cfg)
respGb, err := client.GetDashboardComponents(context.Background(), &pb.GetDashboardComponentsRequest{})
respGb, err := client.GetDashboardComponents(context.Background(), &apiv1.GetDashboardComponentsRequest{})
if err != nil {
t.Errorf("GetDashboardComponentsRequest: %v", err)
@@ -84,7 +84,7 @@ func TestGetActionsAndStart(t *testing.T) {
log.Printf("Response: %+v", respGb)
respSa, err := client.StartAction(context.Background(), &pb.StartActionRequest{ActionId: "blat"})
respSa, err := client.StartAction(context.Background(), &apiv1.StartActionRequest{ActionId: "blat"})
assert.Nil(t, err, "Empty err after start action")
assert.NotNil(t, respSa, "Empty err after start action")

View File

@@ -0,0 +1,62 @@
package grpcapi
import (
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/alexedwards/argon2id"
log "github.com/sirupsen/logrus"
"runtime"
)
var defaultParams = argon2id.Params{
Memory: 64 * 1024,
Iterations: 4,
Parallelism: uint8(runtime.NumCPU()),
SaltLength: 16,
KeyLength: 32,
}
func createHash(password string) (string, error) {
hash, err := argon2id.CreateHash(password, &defaultParams)
if err != nil {
log.Fatal("Error creating hash: ", err)
return "", err
}
return hash, nil
}
func comparePasswordAndHash(password, hash string) bool {
match, err := argon2id.ComparePasswordAndHash(password, hash)
if err != nil {
log.Errorf("Error comparing password and hash: %v", err)
return false
}
return match
}
func checkUserPassword(cfg *config.Config, username, password string) bool {
for _, user := range cfg.AuthLocalUsers.Users {
if user.Username == username {
match := comparePasswordAndHash(password, user.Password)
if match {
return true
} else {
log.WithFields(log.Fields{
"username": username,
}).Warn("Password does not match for user")
return false
}
}
}
log.WithFields(log.Fields{
"username": username,
}).Warn("Failed to check password for user, as username was not found")
return false
}

View File

@@ -7,9 +7,10 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"net/http"
gw "github.com/OliveTin/OliveTin/gen/grpc"
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
cors "github.com/OliveTin/OliveTin/internal/cors"
@@ -45,28 +46,104 @@ func parseHttpHeaderForAuth(req *http.Request) (string, string) {
return username[0], ""
}
//gocyclo:ignore
func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
username := ""
usergroup := ""
provider := "unknown"
sid := ""
if cfg.AuthJwtCookieName != "" {
username, usergroup = parseJwtCookie(req)
provider = "jwt-cookie"
}
if cfg.AuthHttpHeaderUsername != "" {
if cfg.AuthHttpHeaderUsername != "" && username == "" {
username, usergroup = parseHttpHeaderForAuth(req)
provider = "http-header"
}
md := metadata.Pairs(
"username", username,
"usergroup", usergroup,
)
if len(cfg.AuthOAuth2Providers) > 0 && username == "" {
username, usergroup, sid = parseOAuth2Cookie(req)
provider = "oauth2"
}
if cfg.AuthLocalUsers.Enabled && username == "" {
username, usergroup, sid = parseLocalUserCookie(req)
provider = "local"
}
md := metadata.New(map[string]string{
"username": username,
"usergroup": usergroup,
"provider": provider,
"sid": sid,
})
log.Tracef("api request metadata: %+v", md)
return md
}
func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error {
md, ok := runtime.ServerMetadataFromContext(ctx)
if !ok {
log.Warn("Could not get ServerMetadata from context")
return nil
}
forwardResponseHandlerLoginLocalUser(md.HeaderMD, w)
forwardResponseHandlerLogout(md.HeaderMD, w)
return nil
}
func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
if getMetadataKeyOrEmpty(md, "logout-provider") != "" {
sid := getMetadataKeyOrEmpty(md, "logout-sid")
delete(registeredStates, sid)
http.SetCookie(
w,
&http.Cookie{
Name: "olivetin-sid-oauth",
MaxAge: 31556952, // 1 year
Value: "",
HttpOnly: true,
Path: "/",
},
)
deleteLocalUserSession("local", sid)
http.SetCookie(
w,
&http.Cookie{
Name: "olivetin-sid-local",
MaxAge: 31556952, // 1 year
Value: "",
HttpOnly: true,
Path: "/",
},
)
w.Header().Set("Content-Type", "text/html")
// We cannot send a HTTP redirect here, because we don't have access to req.
w.Write([]byte("<script>window.location.href = '/';</script>"))
}
}
func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
mdValues := md.Get(key)
if len(mdValues) > 0 {
return mdValues[0]
}
return ""
}
func SetGlobalRestConfig(config *config.Config) {
cfg = config
}
@@ -74,8 +151,10 @@ func SetGlobalRestConfig(config *config.Config) {
func startRestAPIServer(globalConfig *config.Config) error {
cfg = globalConfig
loadUserSessions()
log.WithFields(log.Fields{
"address": cfg.ListenAddressGrpcActions,
"address": cfg.ListenAddressRestActions,
}).Info("Starting REST API")
mux := newMux()
@@ -87,6 +166,7 @@ func newMux() *runtime.ServeMux {
// The MarshalOptions set some important compatibility settings for the webui. See below.
mux := runtime.NewServeMux(
runtime.WithMetadata(parseRequestMetadata),
runtime.WithForwardResponseOption(forwardResponseHandler),
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
@@ -101,7 +181,7 @@ func newMux() *runtime.ServeMux {
opts := []grpc.DialOption{grpc.WithInsecure()}
err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
err := apiv1.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
if err != nil {
log.Panicf("Could not register REST API Handler %v", err)

View File

@@ -0,0 +1,177 @@
package httpservers
import (
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/filehelper"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"os"
"path/filepath"
"sync"
"time"
)
var sessionStorageMutex sync.Mutex
type UserSession struct {
Username string
Expiry int64
}
type SessionProvider struct {
Sessions map[string]*UserSession
}
type SessionStorage struct {
Providers map[string]*SessionProvider
}
var (
sessionStorage *SessionStorage
)
func registerSessionProviders() {
sessionStorage = &SessionStorage{
Providers: make(map[string]*SessionProvider),
}
registerSessionProvider("local")
registerSessionProvider("oauth2")
}
func registerSessionProvider(provider string) {
sessionStorage.Providers[provider] = &SessionProvider{
Sessions: make(map[string]*UserSession),
}
}
func deleteLocalUserSession(provider string, sid string) {
sessionStorageMutex.Lock()
deleteLocalUserSessionBatch(provider, sid)
sessionStorageMutex.Unlock()
saveUserSessions()
}
func deleteLocalUserSessionBatch(provider string, sid string) {
log.WithFields(log.Fields{
"sid": sid,
"provider": provider,
}).Debug("Deleting user session")
if _, ok := sessionStorage.Providers[provider]; !ok {
return
}
delete(sessionStorage.Providers[provider].Sessions, sid)
}
func registerUserSession(provider string, sid string, username string) {
sessionStorageMutex.Lock()
sessionStorage.Providers[provider].Sessions[sid] = &UserSession{
Username: username,
Expiry: time.Now().Unix() + 31556952, // 1 year
}
sessionStorageMutex.Unlock()
saveUserSessions()
}
func saveUserSessions() {
sessionStorageMutex.Lock()
defer sessionStorageMutex.Unlock()
filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
out, err := yaml.Marshal(sessionStorage)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Errorf("Failed to marshal session data to %v", filename)
return
}
filehelper.WriteFile(filename, out)
}
func loadUserSessions() {
registerSessionProviders()
filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
if _, err := os.Stat(filename); os.IsNotExist(err) {
return
}
data, err := os.ReadFile(filename)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Errorf("Failed to read %v", filename)
return
}
err = yaml.Unmarshal(data, &sessionStorage)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("Failed to unmarshal sessions.local.db")
return
}
deleteExpiredSessions()
}
func deleteExpiredSessions() {
sessionStorageMutex.Lock()
for provider, sessions := range sessionStorage.Providers {
for sid, session := range sessions.Sessions {
if session.Expiry < time.Now().Unix() {
deleteLocalUserSessionBatch(provider, sid)
}
}
}
sessionStorageMutex.Unlock()
saveUserSessions()
}
func getUserFromSession(providerName string, sid string) *config.LocalUser {
provider, ok := sessionStorage.Providers[providerName]
if !ok {
log.WithFields(log.Fields{
"provider": providerName,
}).Warnf("Provider not found")
return nil
}
session, ok := provider.Sessions[sid]
if !ok {
log.WithFields(log.Fields{
"sid": sid,
"provider": providerName,
}).Warnf("Stale session")
return nil
}
user := cfg.FindUserByUsername(session.Username)
if user == nil {
log.WithFields(log.Fields{
"sid": sid,
"provider": providerName,
}).Warnf("User not found")
return nil
}
return user
}

View File

@@ -0,0 +1,54 @@
package httpservers
import (
"google.golang.org/grpc/metadata"
"net/http"
"github.com/google/uuid"
)
func parseLocalUserCookie(req *http.Request) (string, string, string) {
cookie, err := req.Cookie("olivetin-sid-local")
if err != nil {
return "", "", ""
}
cookieValue := cookie.Value
user := getUserFromSession("local", cookieValue)
if user == nil {
return "", "", ""
}
return user.Username, user.Usergroup, cookie.Value
}
func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter) error {
setUsername := getMetadataKeyOrEmpty(md, "set-username")
if setUsername != "" {
user := cfg.FindUserByUsername(setUsername)
if user == nil {
return nil
}
sid := uuid.NewString()
registerUserSession("local", sid, user.Username)
http.SetCookie(
w,
&http.Cookie{
Name: "olivetin-sid-local",
Value: sid,
MaxAge: 31556952, // 1 year
HttpOnly: true,
Path: "/",
},
)
}
return nil
}

View File

@@ -0,0 +1,349 @@
package httpservers
import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"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"
)
var (
registeredStates = make(map[string]*oauth2State)
registeredProviders = make(map[string]*oauth2.Config)
)
type oauth2State struct {
providerConfig *oauth2.Config
providerName string
Username string
Usergroup string
}
func assignIfEmpty(target *string, value string) {
if *target == "" {
*target = value
}
}
func oauth2Init(cfg *config.Config) {
for providerName, providerConfig := range cfg.AuthOAuth2Providers {
completeProviderConfig(providerName, providerConfig)
newConfig := &oauth2.Config{
ClientID: providerConfig.ClientID,
ClientSecret: providerConfig.ClientSecret,
Scopes: providerConfig.Scopes,
Endpoint: oauth2.Endpoint{
AuthURL: providerConfig.AuthUrl,
TokenURL: providerConfig.TokenUrl,
},
RedirectURL: cfg.AuthOAuth2RedirectURL,
}
registeredProviders[providerName] = newConfig
log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
}
}
func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
dbConfig, ok := oauth2ProviderDatabase[providerName]
if ok {
assignIfEmpty(&providerConfig.Name, dbConfig.Name)
assignIfEmpty(&providerConfig.Title, dbConfig.Title)
assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl)
assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl)
assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl)
assignIfEmpty(&providerConfig.Icon, dbConfig.Icon)
assignIfEmpty(&providerConfig.UsernameField, dbConfig.UsernameField)
if providerConfig.Scopes == nil {
providerConfig.Scopes = dbConfig.Scopes
}
} else {
log.Warnf("Provider not found in database: %v", providerName)
}
}
func getOAuth2Config(cfg *config.Config, providerName string) (*oauth2.Config, error) {
config, ok := registeredProviders[providerName]
if !ok {
return nil, fmt.Errorf("Provider not found in config: %v", providerName)
}
return config, nil
}
func randString(nByte int) (string, error) {
b := make([]byte, nByte)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
cookie := &http.Cookie{
Name: name,
Value: value,
MaxAge: 31556952, // 1 year
Secure: r.TLS != nil,
HttpOnly: true,
Path: "/",
}
http.SetCookie(w, cookie)
}
func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
state, err := randString(16)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
providerName := r.URL.Query().Get("provider")
provider, err := getOAuth2Config(cfg, providerName)
if err != nil {
log.Errorf("Failed to get provider config: %v %v", providerName, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
registeredStates[state] = &oauth2State{
providerConfig: provider,
providerName: providerName,
Username: "",
}
setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
}
func 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
}
if 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 := registeredStates[state]
if !ok {
log.Errorf("State not found in server: %v", state)
http.Error(w, "State not found in server", http.StatusBadRequest)
}
return registeredState, state, true
}
type HttpClientSettings struct {
Transport *http.Transport
Timeout time.Duration
}
func getOAuth2HttpClient(providerConfig *config.OAuth2Provider) *HttpClientSettings {
config := &HttpClientSettings{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: providerConfig.InsecureSkipVerify},
},
Timeout: time.Duration(min(3, providerConfig.CallbackTimeout)) * time.Second,
}
if providerConfig.CertBundlePath != "" {
config.Transport.TLSClientConfig.RootCAs = getOAuthCertBundle(providerConfig)
}
return config
}
func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
caCert, err := os.ReadFile(providerConfig.CertBundlePath)
if err != nil {
log.Errorf("OAuth2 Cert Bundle - failed to read file: %v", err)
return nil
}
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
log.Errorf("OAuth2 Cert Bundle - failed to append certificates: %v", err)
}
return caCertPool
}
func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
log.Infof("OAuth2 Callback received")
registeredState, state, ok := checkOAuthCallbackCookie(w, r)
if !ok {
return
}
code := r.FormValue("code")
log.WithFields(log.Fields{
"state": state,
"token-code": code,
}).Debug("OAuth2 Token Code")
providerConfig := cfg.AuthOAuth2Providers[registeredState.providerName]
clientSettings := getOAuth2HttpClient(providerConfig)
exchangeClient := &http.Client{
Transport: clientSettings.Transport,
Timeout: clientSettings.Timeout,
}
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, exchangeClient)
tok, err := registeredState.providerConfig.Exchange(ctx, code)
if err != nil {
log.Errorf("Failed to exchange code: %v", err)
http.Error(w, "Failed to exchange code", http.StatusBadRequest)
return
}
userInfoClient := &http.Client{
Transport: &oauth2.Transport{
Source: registeredState.providerConfig.TokenSource(ctx, tok),
Base: clientSettings.Transport,
},
Timeout: clientSettings.Timeout,
}
userinfo := getUserInfo(userInfoClient, cfg.AuthOAuth2Providers[registeredState.providerName])
registeredStates[state].Username = userinfo.Username
registeredStates[state].Usergroup = userinfo.Usergroup
for k, v := range registeredStates {
log.Debugf("states: %+v %+v", k, v)
}
loginMessage := fmt.Sprintf("OAuth2 login complete for %v", registeredStates[state].Username)
log.WithFields(log.Fields{
"state": state,
}).Infof(loginMessage)
http.Redirect(w, r, "/", http.StatusFound)
w.Write([]byte(loginMessage))
}
type UserInfo struct {
Username string
Usergroup string
}
func getUserInfo(client *http.Client, provider *config.OAuth2Provider) *UserInfo {
ret := &UserInfo{}
res, err := client.Get(provider.WhoamiUrl)
if res.StatusCode != http.StatusOK {
log.Errorf("Failed to get user data: %v", res.StatusCode)
return ret
}
defer res.Body.Close()
contents, err := io.ReadAll(res.Body)
var userData map[string]interface{}
err = json.Unmarshal([]byte(contents), &userData)
if err != nil {
log.Errorf("Failed to unmarshal user data: %v", err)
return ret
}
ret.Username = getDataField(userData, provider.UsernameField)
ret.Usergroup = getDataField(userData, provider.UserGroupField)
return ret
}
func getDataField(data map[string]interface{}, field string) string {
if field == "" {
return ""
}
val, ok := data[field]
if !ok {
log.Errorf("Failed to get field from user data: %v / %v", data, field)
return ""
}
return val.(string)
}
func parseOAuth2Cookie(r *http.Request) (string, string, string) {
cookie, err := r.Cookie("olivetin-sid-oauth")
if err != nil {
log.Warnf("Failed to read OAuth2 cookie: %v", err)
return "", "", ""
}
if cookie.Value == "" {
return "", "", ""
}
serverState, found := registeredStates[cookie.Value]
if !found {
log.WithFields(log.Fields{
"sid": cookie.Value,
"provider": "oauth2",
}).Warnf("Stale session")
return "", "", cookie.Value
}
log.Debugf("Found OAuth2 state: %+v", serverState)
return serverState.Username, serverState.Usergroup, cookie.Value
}

View File

@@ -0,0 +1,26 @@
package httpservers
import (
config "github.com/OliveTin/OliveTin/internal/config"
"golang.org/x/oauth2/endpoints"
)
var oauth2ProviderDatabase = map[string]config.OAuth2Provider{
"github": {
Title: "GitHub",
Name: "github",
Icon: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 128 128\"><g fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M64 5.103c-33.347 0-60.388 27.035-60.388 60.388c0 26.682 17.303 49.317 41.297 57.303c3.017.56 4.125-1.31 4.125-2.905c0-1.44-.056-6.197-.082-11.243c-16.8 3.653-20.345-7.125-20.345-7.125c-2.747-6.98-6.705-8.836-6.705-8.836c-5.48-3.748.413-3.67.413-3.67c6.063.425 9.257 6.223 9.257 6.223c5.386 9.23 14.127 6.562 17.573 5.02c.542-3.903 2.107-6.568 3.834-8.076c-13.413-1.525-27.514-6.704-27.514-29.843c0-6.593 2.36-11.98 6.223-16.21c-.628-1.52-2.695-7.662.584-15.98c0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033c11.526-7.813 16.59-6.19 16.59-6.19c3.287 8.317 1.22 14.46.593 15.98c3.872 4.23 6.215 9.617 6.215 16.21c0 23.194-14.127 28.3-27.574 29.796c2.167 1.874 4.097 5.55 4.097 11.183c0 8.08-.07 14.583-.07 16.572c0 1.607 1.088 3.49 4.148 2.897c23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z\" clip-rule=\"evenodd\"/><path d=\"M26.484 91.806c-.133.3-.605.39-1.035.185c-.44-.196-.685-.605-.543-.906c.13-.31.603-.395 1.04-.188c.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28c-.396-.42-.47-.983-.177-1.254c.298-.266.844-.14 1.24.28c.394.426.472.984.17 1.255zm2.382 3.477c-.37.258-.976.017-1.35-.52c-.37-.538-.37-1.183.01-1.44c.373-.258.97-.025 1.35.507c.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23c-.527-.487-.674-1.18-.343-1.544c.336-.366 1.045-.264 1.564.23c.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486c-.683-.207-1.13-.76-.99-1.238c.14-.477.823-.7 1.512-.485c.683.206 1.13.756.988 1.237m4.943.361c.017.498-.563.91-1.28.92c-.723.017-1.308-.387-1.315-.877c0-.503.568-.91 1.29-.924c.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117c-.7.13-1.35-.172-1.44-.653c-.086-.498.422-.997 1.122-1.126c.714-.123 1.354.17 1.444.663zm0 0\"/></g></svg>",
WhoamiUrl: "https://api.github.com/user",
TokenUrl: endpoints.GitHub.TokenURL,
AuthUrl: endpoints.GitHub.AuthURL,
Scopes: []string{"profile", "email"},
UsernameField: "login",
},
"google": {
Icon: "google",
WhoamiUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
TokenUrl: endpoints.Google.TokenURL,
AuthUrl: endpoints.Google.AuthURL,
Scopes: []string{"profile", "email"},
},
}

View File

@@ -56,6 +56,10 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
websocket.HandleWebsocket(w, r)
})
mux.HandleFunc("/oauth/login", handleOAuthLogin)
mux.HandleFunc("/oauth/callback", handleOAuthCallback)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
logDebugRequest(cfg, "ui ", r)
@@ -73,6 +77,8 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
})
}
oauth2Init(cfg)
srv := &http.Server{
Addr: cfg.ListenAddressSingleHTTPFrontend,
Handler: mux,

View File

@@ -10,8 +10,8 @@ import (
"path/filepath"
config "github.com/OliveTin/OliveTin/internal/config"
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
)
var (
@@ -29,6 +29,13 @@ type webUISettings struct {
PageTitle string
SectionNavigationStyle string
DefaultIconForBack string
SshFoundKey string
SshFoundConfig string
EnableCustomJs bool
AuthLoginUrl string
AuthLocalLogin bool
AuthOAuth2Providers []publicOAuth2Provider
AdditionalLinks []*config.NavigationLink
}
func findWebuiDir() string {
@@ -85,7 +92,7 @@ func setupCustomWebuiDir() {
func generateThemeCss(w http.ResponseWriter, r *http.Request) {
themeCssFilename := path.Join(findCustomWebuiDir(), "themes", cfg.ThemeName, "theme.css")
if !customThemeCssRead {
if !customThemeCssRead || cfg.ThemeCacheDisabled {
customThemeCssRead = true
if _, err := os.Stat(themeCssFilename); err == nil {
@@ -100,19 +107,47 @@ func generateThemeCss(w http.ResponseWriter, r *http.Request) {
w.Write(customThemeCss)
}
type publicOAuth2Provider struct {
Name string
Title string
Icon string
}
func buildPublicOAuth2ProvidersList(cfg *config.Config) []publicOAuth2Provider {
var publicProviders []publicOAuth2Provider
for _, provider := range cfg.AuthOAuth2Providers {
publicProviders = append(publicProviders, publicOAuth2Provider{
Name: provider.Name,
Title: provider.Title,
Icon: provider.Icon,
})
}
return publicProviders
}
func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
jsonRet, _ := json.Marshal(webUISettings{
Rest: cfg.ExternalRestAddress + "/api/",
ShowFooter: cfg.ShowFooter,
ShowNavigation: cfg.ShowNavigation,
ShowNewVersions: cfg.ShowNewVersions,
AvailableVersion: updatecheck.AvailableVersion,
CurrentVersion: updatecheck.CurrentVersion,
AvailableVersion: installationinfo.Runtime.AvailableVersion,
CurrentVersion: installationinfo.Build.Version,
PageTitle: cfg.PageTitle,
SectionNavigationStyle: cfg.SectionNavigationStyle,
DefaultIconForBack: cfg.DefaultIconForBack,
SshFoundKey: installationinfo.Runtime.SshFoundKey,
SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
EnableCustomJs: cfg.EnableCustomJs,
AuthLoginUrl: cfg.AuthLoginUrl,
AuthLocalLogin: cfg.AuthLocalUsers.Enabled,
AuthOAuth2Providers: buildPublicOAuth2ProvidersList(cfg),
AdditionalLinks: cfg.AdditionalNavigationLinks,
})
w.Header().Add("Content-Type", "application/json")
_, err := w.Write([]byte(jsonRet))
if err != nil {
@@ -128,11 +163,24 @@ func startWebUIServer(cfg *config.Config) {
setupCustomWebuiDir()
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.Dir(findWebuiDir())))
mux.Handle("/custom-webui/", http.StripPrefix("/custom-webui/", http.FileServer(http.Dir(findCustomWebuiDir()))))
mux.HandleFunc("/theme.css", generateThemeCss)
mux.HandleFunc("/webUiSettings.json", generateWebUISettings)
webuiDir := findWebuiDir()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
dirName := path.Dir(r.URL.Path)
// Mangle requests for any path like /logs or /config to load the webui index.html
if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
log.Debugf("Mangling request for %s to /index.html", r.URL.Path)
http.ServeFile(w, r, path.Join(webuiDir, "index.html"))
} else {
http.StripPrefix(dirName, http.FileServer(http.Dir(webuiDir))).ServeHTTP(w, r)
}
})
srv := &http.Server{
Addr: cfg.ListenAddressWebUI,
Handler: mux,

View File

@@ -14,5 +14,5 @@ func TestGetWebuiDir(t *testing.T) {
dir := findWebuiDir()
assert.Equal(t, "./webui", dir, "Finding the webui dir")
assert.Equal(t, "../webui/", dir, "Finding the webui dir")
}

View File

@@ -18,7 +18,9 @@ type runtimeInfo struct {
LastBrowserUserAgent string
User string
Uid string
FoundSshKey string
SshFoundKey string
SshFoundConfig string
AvailableVersion string
}
var Runtime = &runtimeInfo{
@@ -28,20 +30,42 @@ var Runtime = &runtimeInfo{
OSReleasePrettyName: getOsReleasePrettyName(),
User: os.Getenv("USER"),
Uid: os.Getenv("UID"),
SshFoundKey: searchForSshKey(),
SshFoundConfig: searchForSshConfig(),
}
func refreshRuntimeInfo() {
Runtime.FoundSshKey = searchForSshKey()
func fileExists(path string) bool {
if _, err := os.Stat(path); err == nil {
return true
}
return false
}
func searchForSshKey() string {
path, _ := filepath.Abs(path.Join(os.Getenv("HOME"), ".ssh/id_rsa"))
if fileExists("/config/ssh/id_rsa") {
return "/config/ssh/id_rsa"
}
return searchForHomeFile(".ssh/id_rsa")
}
func searchForSshConfig() string {
if fileExists("/config/ssh/config") {
return "/config/ssh/config"
}
return searchForHomeFile(".ssh/config")
}
func searchForHomeFile(file string) string {
path, _ := filepath.Abs(path.Join(os.Getenv("HOME"), file))
if _, err := os.Stat(path); err == nil {
return path
}
return "none-found at " + path
return "not found at " + path
}
func isInContainer() bool {

View File

@@ -43,8 +43,6 @@ func configToSosreport(cfg *config.Config) *sosReportConfig {
}
func GetSosReport() string {
refreshRuntimeInfo()
ret := ""
ret += "### SOSREPORT START (copy all text to SOSREPORT END)\n"

View File

@@ -107,12 +107,10 @@ func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *exec
}).Infof("Executing action from calendar")
req := &executor.ExecutionRequest{
Action: action,
Cfg: cfg,
Tags: []string{"calendar"},
AuthenticatedUser: &acl.AuthenticatedUser{
Username: "calendar",
},
Action: action,
Cfg: cfg,
Tags: []string{},
AuthenticatedUser: acl.UserFromSystem(cfg, "calendar"),
}
ex.ExecRequest(req)

Some files were not shown because too many files have changed in this diff Show More