James Read 4 месяцев назад
Родитель
Сommit
235493e471

+ 1 - 0
README.md

@@ -10,6 +10,7 @@
 [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
 
 [![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
+[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%201%20of%205%20(assistance--only)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/)
 
 [OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
 </div>

+ 6 - 0
SECURITY.md

@@ -31,6 +31,12 @@ Please use responsible disclosure practices when reporting a vulnerability. **Yo
 
 * **Option B**: Please email `contact@jread.com` for responsible disclosure. 
 
+The following notes might be helpful when reporting a vulnerability:
+
+* OliveTin does not offer a bug bounty program.
+* GitHub usernames are how we you will be credited for discoveries reported via GitHub, if using emails we'll ask for your preferred name/handle to credit you with.
+* CVEs will be requested via GitHub Security Advisories when appropriate, but we do not guarantee that all vulnerabilities will receive CVEs, as this is determined on a case-by-case basis.
+
 ## Disclosure of how vulnerabilities were found
 
 It is incredibly useful to not just patch security vulnerabilities, but also to understand how they were found. If you are able to share this information, it can help us and the community to better understand potential attack vectors and improve the overall security of the project.

+ 13 - 1
frontend/js/OutputTerminal.js

@@ -1,5 +1,6 @@
 import { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
+import { WebLinksAddon } from '@xterm/addon-web-links'
 import { Mutex } from './Mutex.js'
 
 /**
@@ -18,13 +19,24 @@ export class OutputTerminal {
   constructor (executionTrackingId) {
     this.executionTrackingId = executionTrackingId
     this.writeMutex = new Mutex()
+    const linkHandler = {
+      activate (event, text, _range) {
+        event.preventDefault()
+        window.open(text, '_blank')
+      }
+    }
+
     this.terminal = new Terminal({
-      convertEol: true
+      convertEol: true,
+      linkHandler
     })
 
     const fitAddon = new FitAddon()
     this.terminal.loadAddon(fitAddon)
     this.terminal.fit = fitAddon
+
+    this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri)))
+    this.linkHandlerConfigured = true
   }
 
   async write (out, then) {

+ 10 - 3
frontend/package-lock.json

@@ -15,6 +15,7 @@
 				"@hugeicons/vue": "^1.0.4",
 				"@vitejs/plugin-vue": "^6.0.4",
 				"@xterm/addon-fit": "^0.11.0",
+				"@xterm/addon-web-links": "^0.12.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
 				"picocrank": "^1.14.0",
@@ -1570,6 +1571,12 @@
 			"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
 			"license": "MIT"
 		},
+		"node_modules/@xterm/addon-web-links": {
+			"version": "0.12.0",
+			"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
+			"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
+			"license": "MIT"
+		},
 		"node_modules/@xterm/xterm": {
 			"version": "6.0.0",
 			"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
@@ -4479,9 +4486,9 @@
 			}
 		},
 		"node_modules/minimatch": {
-			"version": "3.1.2",
-			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-			"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+			"version": "3.1.5",
+			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+			"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
 			"license": "ISC",
 			"dependencies": {
 				"brace-expansion": "^1.1.7"

+ 2 - 1
frontend/package.json

@@ -28,13 +28,14 @@
 		"@hugeicons/vue": "^1.0.4",
 		"@vitejs/plugin-vue": "^6.0.4",
 		"@xterm/addon-fit": "^0.11.0",
+		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
 		"picocrank": "^1.14.0",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^31.0.0",
 		"vite": "^7.3.1",
-    "vue": "^3.5.29",
+		"vue": "^3.5.29",
 		"vue-i18n": "^11.2.8",
 		"vue-router": "^5.0.3"
 	}

+ 5 - 1
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts

@@ -210,6 +210,11 @@ export declare type EffectivePolicy = Message<"olivetin.api.v1.EffectivePolicy">
    * @generated from field: bool show_log_list = 2;
    */
   showLogList: boolean;
+
+  /**
+   * @generated from field: bool show_version_number = 3;
+   */
+  showVersionNumber: boolean;
 };
 
 /**
@@ -1853,4 +1858,3 @@ export declare const OliveTinApiService: GenService<{
     output: typeof EntitySchema;
   },
 }>;
-

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js


+ 11 - 9
frontend/resources/vue/App.vue

@@ -31,7 +31,7 @@
             <footer title="footer" v-if="showFooter">
                 <p>
                     <img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
-                    OliveTin {{ currentVersion }}
+                    OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
                 </p>
                 <p>
                     <span>
@@ -52,7 +52,7 @@
 
                     <span>{{ t('connected') }}</span>
                 </p>
-                <p>
+                <p v-if="showVersionNumber">
                     <a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
                 </p>
             </footer>
@@ -68,7 +68,7 @@
                 </option>
             </select>
             <p class="browser-languages">
-                {{ t('language-dialog.browser-languages') }}: 
+                {{ t('language-dialog.browser-languages') }}:
                 <span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
                 <span v-else>{{ t('language-dialog.not-available') }}</span>
             </p>
@@ -126,6 +126,7 @@ const showFooter = ref(true)
 const showNavigation = ref(true)
 const showLogs = ref(true)
 const showDiagnostics = ref(true)
+const showVersionNumber = ref(true)
 const showLoginLink = ref(true)
 const sectionNavigationStyle = ref('sidebar')
 
@@ -184,7 +185,7 @@ function normalizeBrowserLanguage() {
     if (navigator.languages && navigator.languages.length > 0) {
         for (const candidate of navigator.languages) {
             const lowerCandidate = candidate.toLowerCase()
-            
+
             // Try exact match (case-insensitive)
             const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
             if (exact) {
@@ -223,6 +224,7 @@ function updateHeaderFromInit() {
     showNavigation.value = window.initResponse.showNavigation
     showLogs.value = window.initResponse.showLogList
     showDiagnostics.value = window.initResponse.showDiagnostics
+    showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
     sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
     availableThemes.value = window.initResponse.availableThemes || []
 
@@ -277,7 +279,7 @@ function renderNavigation() {
 
 function openLanguageDialog() {
     selectedLanguage.value = languagePreference.value
-    
+
     if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
         browserLanguages.value = navigator.languages
     } else {
@@ -327,7 +329,7 @@ function handleLanguageDialogClick(event) {
 
 function openThemeDialog() {
     selectedTheme.value = themePreference.value || ''
-    
+
     if (themeDialog.value) {
         themeDialog.value.showModal()
     }
@@ -354,7 +356,7 @@ function changeTheme() {
 
 function applyTheme() {
     let themeStyle = document.getElementById('theme-style')
-    
+
     if (!themeStyle) {
         themeStyle = document.createElement('style')
         themeStyle.id = 'theme-style'
@@ -404,10 +406,10 @@ window.updateHeaderFromInit = updateHeaderFromInit
 onMounted(() => {
     serverConnection.value = true;
     updateHeaderFromInit()
-    
+
     // Initialize selected language from stored preference
     selectedLanguage.value = languagePreference.value
-    
+
     // Initialize selected theme from stored preference
     selectedTheme.value = themePreference.value || ''
 

+ 5 - 2
frontend/resources/vue/views/DiagnosticsView.vue

@@ -162,7 +162,10 @@ async function generateBrowserInfo() {
       userAgentData: userAgentData
     }
 
-    const olivetinVersion = window.initResponse?.currentVersion || t('diagnostics.unknown')
+    const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
+    const olivetinVersion = showVersionNumber
+      ? (window.initResponse?.currentVersion || t('diagnostics.unknown'))
+      : '[hidden]'
     const currentLanguage = locale.value || t('diagnostics.unknown')
 
     let output = '';
@@ -300,4 +303,4 @@ onMounted(() => {
   flex-direction: column;
   gap: 1em;
 }
-</style>
+</style>

+ 13 - 0
integration-tests/tests/xtermLinkHandling/config.yaml

@@ -0,0 +1,13 @@
+#
+# Integration Test Config: xterm link handling
+#
+
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+  - title: Echo URL
+    shell: echo "See https://example.com for more info"
+    popupOnStart: execution-dialog-stdout-only

+ 69 - 0
integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs

@@ -0,0 +1,69 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import { By, Condition } from 'selenium-webdriver'
+import {
+  getRootAndWait,
+  takeScreenshotOnFailure,
+  getTerminalBuffer,
+} from '../../lib/elements.js'
+
+describe('config: xtermLinkHandling', function () {
+  before(async function () {
+    await runner.start('xtermLinkHandling')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver)
+  })
+
+  it('xterm output shows URL and link handling is configured', async function () {
+    await getRootAndWait()
+
+    await webdriver.wait(new Condition('wait for Echo URL button', async () => {
+      const btns = await webdriver.findElements(By.css('[title="Echo URL"]'))
+      return btns.length === 1
+    }), 10000)
+
+    const echoUrlButton = await webdriver.findElement(By.css('[title="Echo URL"]'))
+    await echoUrlButton.click()
+
+    await webdriver.wait(new Condition('wait for execution view', async () => {
+      const url = await webdriver.getCurrentUrl()
+      return url.includes('/logs/') && !url.endsWith('/logs')
+    }), 10000)
+
+    await webdriver.wait(new Condition('wait for execution status', async () => {
+      const statusElements = await webdriver.findElements(By.id('execution-dialog-status'))
+      return statusElements.length > 0
+    }), 5000)
+
+    await webdriver.wait(new Condition('wait for execution to finish', async () => {
+      try {
+        const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
+        const statusText = await statusElement.getText()
+        return !statusText.includes('Executing')
+      } catch (e) {
+        return false
+      }
+    }), 5000)
+
+    await webdriver.sleep(500)
+
+    const bufferText = await getTerminalBuffer()
+    expect(bufferText).to.not.be.null
+    expect(bufferText).to.include('https://example.com')
+
+    const linkHandlerSet = await webdriver.executeScript(`
+      try {
+        return !!(window.terminal && window.terminal.linkHandlerConfigured === true)
+      } catch (e) {
+        return false
+      }
+    `)
+    expect(linkHandlerSet).to.equal(true)
+  })
+})

+ 1 - 0
proto/olivetin/api/v1/olivetin.proto

@@ -51,6 +51,7 @@ message GetDashboardResponse {
 message EffectivePolicy {
 	bool show_diagnostics = 1;
 	bool show_log_list = 2;
+	bool show_version_number = 3;
 }
 
 message GetDashboardRequest {

+ 16 - 7
service/gen/olivetin/api/v1/olivetin.pb.go

@@ -410,11 +410,12 @@ func (x *GetDashboardResponse) GetDashboard() *Dashboard {
 }
 
 type EffectivePolicy struct {
-	state           protoimpl.MessageState `protogen:"open.v1"`
-	ShowDiagnostics bool                   `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
-	ShowLogList     bool                   `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
-	unknownFields   protoimpl.UnknownFields
-	sizeCache       protoimpl.SizeCache
+	state             protoimpl.MessageState `protogen:"open.v1"`
+	ShowDiagnostics   bool                   `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
+	ShowLogList       bool                   `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
+	ShowVersionNumber bool                   `protobuf:"varint,3,opt,name=show_version_number,json=showVersionNumber,proto3" json:"show_version_number,omitempty"`
+	unknownFields     protoimpl.UnknownFields
+	sizeCache         protoimpl.SizeCache
 }
 
 func (x *EffectivePolicy) Reset() {
@@ -461,6 +462,13 @@ func (x *EffectivePolicy) GetShowLogList() bool {
 	return false
 }
 
+func (x *EffectivePolicy) GetShowVersionNumber() bool {
+	if x != nil {
+		return x.ShowVersionNumber
+	}
+	return false
+}
+
 type GetDashboardRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
@@ -3934,10 +3942,11 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"f\n" +
 	"\x14GetDashboardResponse\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x128\n" +
-	"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"`\n" +
+	"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"\x90\x01\n" +
 	"\x0fEffectivePolicy\x12)\n" +
 	"\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
-	"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\"k\n" +
+	"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\x12.\n" +
+	"\x13show_version_number\x18\x03 \x01(\bR\x11showVersionNumber\"k\n" +
 	"\x13GetDashboardRequest\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1f\n" +
 	"\ventity_type\x18\x02 \x01(\tR\n" +

+ 8 - 8
service/go.mod

@@ -1,6 +1,6 @@
 module github.com/OliveTin/OliveTin
 
-go 1.25.0
+go 1.25.6
 
 exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
 
@@ -10,7 +10,7 @@ require (
 	github.com/MicahParks/keyfunc/v3 v3.8.0
 	github.com/PaesslerAG/jsonpath v0.1.1
 	github.com/alexedwards/argon2id v1.0.0
-	github.com/bufbuild/buf v1.65.0
+	github.com/bufbuild/buf v1.66.0
 	github.com/fsnotify/fsnotify v1.9.0
 	github.com/fzipp/gocyclo v0.6.0
 	github.com/go-critic/go-critic v0.14.3
@@ -27,7 +27,7 @@ require (
 	github.com/sirupsen/logrus v1.9.4
 	github.com/stretchr/testify v1.11.1
 	go.akshayshah.org/connectproto v0.6.0
-	golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
+	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	golang.org/x/oauth2 v0.35.0
 	golang.org/x/sys v0.41.0
 	google.golang.org/protobuf v1.36.11
@@ -45,7 +45,7 @@ require (
 	buf.build/go/bufplugin v0.9.0 // indirect
 	buf.build/go/bufprivateusage v0.1.0 // indirect
 	buf.build/go/interrupt v1.1.0 // indirect
-	buf.build/go/protovalidate v1.1.2 // indirect
+	buf.build/go/protovalidate v1.1.3 // indirect
 	buf.build/go/protoyaml v0.6.0 // indirect
 	buf.build/go/spdx v0.2.0 // indirect
 	buf.build/go/standard v0.1.0 // indirect
@@ -57,7 +57,7 @@ require (
 	github.com/PaesslerAG/gval v1.2.4 // 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.2-0.20260130195850-5c64bed4577e // indirect
+	github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 // indirect
 	github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cli/browser v1.3.0 // indirect
@@ -90,7 +90,7 @@ require (
 	github.com/gofrs/flock v0.13.0 // indirect
 	github.com/google/cel-go v0.27.0 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
-	github.com/google/go-containerregistry v0.20.7 // indirect
+	github.com/google/go-containerregistry v0.21.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jdx/go-netrc v1.0.0 // indirect
@@ -155,8 +155,8 @@ require (
 	golang.org/x/text v0.34.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
 	golang.org/x/tools v0.42.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
 	google.golang.org/grpc v1.75.1 // indirect
 	mvdan.cc/xurls/v2 v2.6.0 // indirect
 	pluginrpc.com/pluginrpc v0.5.0 // indirect

+ 15 - 0
service/go.sum

@@ -32,6 +32,8 @@ buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY
 buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
 buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0=
 buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE=
+buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE=
+buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE=
 buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
 buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
 buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
@@ -84,6 +86,8 @@ github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs=
 github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw=
 github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=
 github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=
+github.com/bufbuild/buf v1.66.0 h1:6kksYJpu6r45bvPJSTwNSwRqiAjrwB9YyU7skjNzFVo=
+github.com/bufbuild/buf v1.66.0/go.mod h1:tWVlwtIPZ7kzlCB9D0hbbfrroT0GNCybPdPQXq1i1Ac=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
@@ -92,6 +96,8 @@ github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREB
 github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
+github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 h1:nxt1QhP9rMQNFhHTdcNFwJ9wKCSdBjd28gz+qGDv4kM=
+github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -200,6 +206,8 @@ 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.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
 github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
+github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI=
+github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
@@ -375,6 +383,7 @@ go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIl
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
 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.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
@@ -421,6 +430,8 @@ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7
 golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
+golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
+golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
 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-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk=
@@ -519,6 +530,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
@@ -527,6 +540,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
 google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

+ 13 - 4
service/internal/api/api.go

@@ -710,7 +710,9 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAm
 }
 
 func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
-	sos := installationinfo.GetSosReport()
+	user := auth.UserFromApiCall(ctx, req, api.cfg)
+	redactVersion := !user.EffectivePolicy.ShowVersionNumber
+	sos := installationinfo.GetSosReport(redactVersion)
 
 	if !api.cfg.InsecureAllowDumpSos {
 		log.Info(sos)
@@ -914,12 +916,19 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
 
 	loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
 
+	showVersion := user.EffectivePolicy.ShowVersionNumber
+	currentVersion := ""
+	availableVersion := ""
+	if showVersion {
+		currentVersion = installationinfo.Build.Version
+		availableVersion = installationinfo.Runtime.AvailableVersion
+	}
 	res := &apiv1.InitResponse{
 		ShowFooter:                api.cfg.ShowFooter,
 		ShowNavigation:            api.cfg.ShowNavigation,
-		ShowNewVersions:           api.cfg.ShowNewVersions,
-		AvailableVersion:          installationinfo.Runtime.AvailableVersion,
-		CurrentVersion:            installationinfo.Build.Version,
+		ShowNewVersions:           showVersion && api.cfg.ShowNewVersions,
+		AvailableVersion:          availableVersion,
+		CurrentVersion:            currentVersion,
 		PageTitle:                 api.cfg.PageTitle,
 		SectionNavigationStyle:    api.cfg.SectionNavigationStyle,
 		DefaultIconForBack:        api.cfg.DefaultIconForBack,

+ 3 - 2
service/internal/api/apiActions.go

@@ -55,8 +55,9 @@ func matchesEntity(binding *executor.ActionBinding, entity *entities.Entity) boo
 
 func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
 	ret := &apiv1.EffectivePolicy{
-		ShowDiagnostics: policy.ShowDiagnostics,
-		ShowLogList:     policy.ShowLogList,
+		ShowDiagnostics:   policy.ShowDiagnostics,
+		ShowLogList:       policy.ShowLogList,
+		ShowVersionNumber: policy.ShowVersionNumber,
 	}
 
 	return ret

+ 7 - 2
service/internal/auth/authpublic/authenticateduser.go

@@ -76,8 +76,9 @@ func (u *AuthenticatedUser) BuildUserAcls(cfg *config.Config) {
 
 func getEffectivePolicy(cfg *config.Config, u *AuthenticatedUser) *config.ConfigurationPolicy {
 	ret := &config.ConfigurationPolicy{
-		ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
-		ShowLogList:     cfg.DefaultPolicy.ShowLogList,
+		ShowDiagnostics:   cfg.DefaultPolicy.ShowDiagnostics,
+		ShowLogList:       cfg.DefaultPolicy.ShowLogList,
+		ShowVersionNumber: cfg.DefaultPolicy.ShowVersionNumber,
 	}
 
 	for _, acl := range cfg.AccessControlLists {
@@ -98,5 +99,9 @@ func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.Con
 		ret.ShowLogList = policy.ShowLogList
 	}
 
+	if policy.ShowVersionNumber {
+		ret.ShowVersionNumber = policy.ShowVersionNumber
+	}
+
 	return ret
 }

+ 27 - 17
service/internal/auth/otoauth2/restapi_auth_oauth2.go

@@ -11,6 +11,7 @@ import (
 	"io"
 	"net/http"
 	"os"
+	"sync"
 	"time"
 
 	authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
@@ -21,6 +22,7 @@ import (
 
 type OAuth2Handler struct {
 	cfg                 *config.Config
+	mu                  sync.RWMutex
 	registeredStates    map[string]*oauth2State
 	registeredProviders map[string]*oauth2.Config
 }
@@ -144,11 +146,13 @@ func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	h.mu.Lock()
 	h.registeredStates[state] = &oauth2State{
 		providerConfig: provider,
 		providerName:   providerName,
 		Username:       "",
 	}
+	h.mu.Unlock()
 
 	h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
 
@@ -177,7 +181,9 @@ func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.
 		return nil, state, false
 	}
 
+	h.mu.RLock()
 	registeredState, ok := h.registeredStates[state]
+	h.mu.RUnlock()
 	if !ok {
 		log.Errorf("State not found in server: %v", state)
 		http.Error(w, "State not found in server", http.StatusBadRequest)
@@ -287,8 +293,10 @@ func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Reque
 	userInfoClient := h.createUserInfoClient(ctx, registeredState.providerConfig, tok, clientSettings)
 	userinfo := getUserInfo(h.cfg, userInfoClient, providerConfig)
 
+	h.mu.Lock()
 	h.registeredStates[state].Username = userinfo.Username
 	h.registeredStates[state].Usergroup = h.computeUsergroup(userinfo, providerConfig)
+	h.mu.Unlock()
 
 	http.Redirect(w, r, "/", http.StatusFound)
 }
@@ -366,34 +374,36 @@ func getDataField(data map[string]any, field string) string {
 	return stringVal
 }
 
-func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
-	cookie, err := context.Request.Cookie("olivetin-sid-oauth")
-
-	user := &authTypes.AuthenticatedUser{}
-
-	if err != nil {
-		return nil
+func (h *OAuth2Handler) lookupOAuth2UserByState(state string) (*authTypes.AuthenticatedUser, bool) {
+	h.mu.RLock()
+	serverState, found := h.registeredStates[state]
+	if !found {
+		h.mu.RUnlock()
+		return nil, false
+	}
+	user := &authTypes.AuthenticatedUser{
+		Username:      serverState.Username,
+		UsergroupLine: serverState.Usergroup,
+		Provider:      "oauth2",
+		SID:           state,
 	}
+	h.mu.RUnlock()
+	return user, true
+}
 
-	if cookie.Value == "" {
+func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
+	cookie, err := context.Request.Cookie("olivetin-sid-oauth")
+	if err != nil || cookie.Value == "" {
 		return nil
 	}
 
-	serverState, found := h.registeredStates[cookie.Value]
-
+	user, found := h.lookupOAuth2UserByState(cookie.Value)
 	if !found {
 		log.WithFields(log.Fields{
 			"sid":      cookie.Value,
 			"provider": "oauth2",
 		}).Warnf("Stale session")
-
 		return nil
 	}
-
-	user.Username = serverState.Username
-	user.UsergroupLine = serverState.Usergroup
-	user.Provider = "oauth2"
-	user.SID = cookie.Value
-
 	return user
 }

+ 4 - 2
service/internal/config/config.go

@@ -98,8 +98,9 @@ type AccessControlList struct {
 
 // ConfigurationPolicy defines global settings which are overridden with an ACL.
 type ConfigurationPolicy struct {
-	ShowDiagnostics bool `koanf:"showDiagnostics"`
-	ShowLogList     bool `koanf:"showLogList"`
+	ShowDiagnostics   bool `koanf:"showDiagnostics"`
+	ShowLogList       bool `koanf:"showLogList"`
+	ShowVersionNumber bool `koanf:"showVersionNumber"`
 }
 
 type PrometheusConfig struct {
@@ -297,6 +298,7 @@ func DefaultConfigWithBasePort(basePort int) *Config {
 
 	config.DefaultPolicy.ShowDiagnostics = true
 	config.DefaultPolicy.ShowLogList = true
+	config.DefaultPolicy.ShowVersionNumber = true
 
 	return &config
 }

+ 1 - 0
service/internal/config/sanitize.go

@@ -164,6 +164,7 @@ func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
 		cfg.DefaultPermissions.View = false
 		cfg.DefaultPermissions.Exec = false
 		cfg.DefaultPermissions.Logs = false
+		cfg.DefaultPermissions.Kill = false
 	}
 }
 

+ 11 - 3
service/internal/installationinfo/sosreport.go

@@ -40,15 +40,23 @@ func configToSosreport(cfg *config.Config) *sosReportConfig {
 	}
 }
 
-func GetSosReport() string {
+func GetSosReport(redactVersion bool) string {
 	ret := ""
 
 	ret += "### SOSREPORT START (copy all text to SOSREPORT END)\n"
 
-	out, _ := yaml.Marshal(Build)
+	buildForReport := *Build
+	if redactVersion {
+		buildForReport.Version = "[redacted]"
+	}
+	out, _ := yaml.Marshal(&buildForReport)
 	ret += fmt.Sprintf("# Build: \n%+v\n", string(out))
 
-	out, _ = yaml.Marshal(Runtime)
+	runtimeForReport := *Runtime
+	if redactVersion {
+		runtimeForReport.AvailableVersion = "[redacted]"
+	}
+	out, _ = yaml.Marshal(&runtimeForReport)
 	ret += fmt.Sprintf("# Runtime:\n%+v\n", string(out))
 
 	out, _ = yaml.Marshal(configToSosreport(Config))

Некоторые файлы не были показаны из-за большого количества измененных файлов