feature: Policy support - allow hiding daignostics and logs (#569)

This commit is contained in:
James Read 2025-04-20 00:05:49 +01:00 committed by GitHub
parent f02982b451
commit c19428f6b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 193 additions and 29 deletions

View File

@ -0,0 +1,14 @@
# Integration Test Config: Policy All False
#
logLevel: "DEBUG"
checkForUpdates: false
defaultPolicy:
showDiagnostics: false
showLogList: false
actions:
- title: sleep 2 seconds
shell: sleep 2
icon: "&#x1F971"

View File

@ -37,6 +37,15 @@ describe('config: general', function () {
*/ */
}) })
it('navbar contains default policy links', async function () {
await getRootAndWait()
const logListLink = await webdriver.findElements(By.css('[href="/logs"]'))
expect(logListLink).to.not.be.empty
const diagnosticsLink = await webdriver.findElements(By.css('[href="/diagnostics"]'))
expect(diagnosticsLink).to.not.be.empty
})
it('Footer contains promo', async function () { it('Footer contains promo', async function () {
const ftr = await webdriver.findElement(By.tagName('footer')).getText() const ftr = await webdriver.findElement(By.tagName('footer')).getText()

View File

@ -0,0 +1,32 @@
import {
getRootAndWait,
takeScreenshotOnFailure,
} from '../lib/elements.js'
import { By } from 'selenium-webdriver'
import { expect } from 'chai'
describe('config: policy-all-false', function () {
before(async function () {
await runner.start('policy-all-false')
});
after(async () => {
await runner.stop()
});
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('navbar should not contain default policy links', async function () {
await getRootAndWait()
const logListLink = await webdriver.findElements(By.css('[href="/logs"]'))
expect(logListLink).to.be.empty
const diagnosticsLink = await webdriver.findElements(By.css('[href="/diagnostics"]'))
expect(diagnosticsLink).to.be.empty
})
})

View File

@ -49,6 +49,19 @@ message GetDashboardComponentsResponse {
string authenticated_user = 5; string authenticated_user = 5;
string authenticated_user_provider = 6; string authenticated_user_provider = 6;
EffectivePolicy effective_policy = 7;
Diagnostics diagnostics = 8;
}
message Diagnostics {
string SshFoundKey = 1;
string SshFoundConfig = 2;
}
message EffectivePolicy {
bool show_diagnostics = 1;
bool show_log_list = 2;
} }
message GetDashboardComponentsRequest {} message GetDashboardComponentsRequest {}

View File

@ -32,6 +32,8 @@ type AuthenticatedUser struct {
SID string SID string
Acls []string Acls []string
EffectivePolicy *config.ConfigurationPolicy
} }
func (u *AuthenticatedUser) IsGuest() bool { func (u *AuthenticatedUser) IsGuest() bool {
@ -43,15 +45,22 @@ func logAclNotMatched(cfg *config.Config, aclFunction string, user *Authenticate
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"User": user.Username, "User": user.Username,
"Action": action.Title, "Action": action.Title,
}).Debugf("%v - No ACLs Matched", aclFunction) "ACL": acl.Name,
}).Debugf("%v - ACL Not Matched", aclFunction)
} }
} }
func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) { func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
actionTitle := "N/A"
if action != nil {
actionTitle = action.Title
}
if cfg.LogDebugOptions.AclMatched { if cfg.LogDebugOptions.AclMatched {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"User": user.Username, "User": user.Username,
"Action": action.Title, "Action": actionTitle,
"ACL": acl.Name, "ACL": acl.Name,
}).Debugf("%v - Matched ACL", aclFunction) }).Debugf("%v - Matched ACL", aclFunction)
} }
@ -209,6 +218,8 @@ func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
continue continue
} }
} }
user.EffectivePolicy = getEffectivePolicy(cfg, user)
} }
func hasGroupsMatch(matchUsergroups []string, usergroup string) bool { func hasGroupsMatch(matchUsergroups []string, usergroup string) bool {
@ -249,3 +260,32 @@ func getRelevantAcls(cfg *config.Config, actionAcls []string, user *Authenticate
return ret return ret
} }
func getEffectivePolicy(cfg *config.Config, user *AuthenticatedUser) *config.ConfigurationPolicy {
ret := &config.ConfigurationPolicy{
ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
ShowLogList: cfg.DefaultPolicy.ShowLogList,
}
for _, acl := range cfg.AccessControlLists {
if slices.Contains(user.Acls, acl.Name) {
logAclMatched(cfg, "GetEffectivePolicy", user, nil, acl)
ret = buildConfigurationPolicy(ret, acl.Policy)
}
}
return ret
}
func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy {
if policy.ShowDiagnostics {
ret.ShowDiagnostics = policy.ShowDiagnostics
}
if policy.ShowLogList {
ret.ShowLogList = policy.ShowLogList
}
return ret
}

View File

@ -76,6 +76,13 @@ type AccessControlList struct {
MatchUsergroups []string MatchUsergroups []string
MatchUsernames []string MatchUsernames []string
Permissions PermissionsList Permissions PermissionsList
Policy ConfigurationPolicy
}
// ConfigurationPolicy defines global settings which are overridden with an ACL.
type ConfigurationPolicy struct {
ShowDiagnostics bool
ShowLogList bool
} }
type PrometheusConfig struct { type PrometheusConfig struct {
@ -123,6 +130,7 @@ type Config struct {
AuthOAuth2RedirectURL string AuthOAuth2RedirectURL string
AuthOAuth2Providers map[string]*OAuth2Provider AuthOAuth2Providers map[string]*OAuth2Provider
DefaultPermissions PermissionsList DefaultPermissions PermissionsList
DefaultPolicy ConfigurationPolicy
AccessControlLists []*AccessControlList AccessControlLists []*AccessControlList
WebUIDir string WebUIDir string
CronSupportForSeconds bool CronSupportForSeconds bool
@ -243,5 +251,8 @@ func DefaultConfigWithBasePort(basePort int) *Config {
config.ListenAddressWebUI = fmt.Sprintf("localhost:%d", basePort+3) config.ListenAddressWebUI = fmt.Sprintf("localhost:%d", basePort+3)
config.ListenAddressPrometheus = fmt.Sprintf("localhost:%d", basePort+4) config.ListenAddressPrometheus = fmt.Sprintf("localhost:%d", basePort+4)
config.DefaultPolicy.ShowDiagnostics = true
config.DefaultPolicy.ShowLogList = true
return &config return &config
} }

View File

@ -5,6 +5,7 @@ import (
acl "github.com/OliveTin/OliveTin/internal/acl" acl "github.com/OliveTin/OliveTin/internal/acl"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
executor "github.com/OliveTin/OliveTin/internal/executor" executor "github.com/OliveTin/OliveTin/internal/executor"
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
sv "github.com/OliveTin/OliveTin/internal/stringvariables" sv "github.com/OliveTin/OliveTin/internal/stringvariables"
"sort" "sort"
) )
@ -35,9 +36,32 @@ func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl
} }
}) })
res.EffectivePolicy = buildEffectivePolicy(user.EffectivePolicy)
res.Diagnostics = buildDiagnostics(res.EffectivePolicy.ShowDiagnostics)
return res return res
} }
func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
ret := &apiv1.EffectivePolicy{
ShowDiagnostics: policy.ShowDiagnostics,
ShowLogList: policy.ShowLogList,
}
return ret
}
func buildDiagnostics(showDiagnostics bool) *apiv1.Diagnostics {
ret := &apiv1.Diagnostics{}
if showDiagnostics {
ret.SshFoundKey = installationinfo.Runtime.SshFoundKey
ret.SshFoundConfig = installationinfo.Runtime.SshFoundConfig
}
return ret
}
func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *apiv1.Action { func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *apiv1.Action {
action := actionBinding.Action action := actionBinding.Action

View File

@ -29,8 +29,6 @@ type webUISettings struct {
PageTitle string PageTitle string
SectionNavigationStyle string SectionNavigationStyle string
DefaultIconForBack string DefaultIconForBack string
SshFoundKey string
SshFoundConfig string
EnableCustomJs bool EnableCustomJs bool
AuthLoginUrl string AuthLoginUrl string
AuthLocalLogin bool AuthLocalLogin bool
@ -138,8 +136,6 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
PageTitle: cfg.PageTitle, PageTitle: cfg.PageTitle,
SectionNavigationStyle: cfg.SectionNavigationStyle, SectionNavigationStyle: cfg.SectionNavigationStyle,
DefaultIconForBack: cfg.DefaultIconForBack, DefaultIconForBack: cfg.DefaultIconForBack,
SshFoundKey: installationinfo.Runtime.SshFoundKey,
SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
EnableCustomJs: cfg.EnableCustomJs, EnableCustomJs: cfg.EnableCustomJs,
AuthLoginUrl: cfg.AuthLoginUrl, AuthLoginUrl: cfg.AuthLoginUrl,
AuthLocalLogin: cfg.AuthLocalUsers.Enabled, AuthLocalLogin: cfg.AuthLocalUsers.Enabled,

View File

@ -35,12 +35,6 @@
</ul> </ul>
<ul id = "supplemental-links"> <ul id = "supplemental-links">
<li title = "Diagnostics">
<a id = "showDiagnostics">Diagnostics</a>
</li>
<li title = "Logs">
<a id = "showLogs">Logs</a>
</li>
</ul> </ul>
</nav> </nav>

View File

@ -0,0 +1,33 @@
export class NavigationBar {
constructor() {
this.navbar = document.getElementsByTagName('nav')[0]
this.mainLinks = document.getElementById('navigation-links')
this.supplementalLinks = document.getElementById('supplemental-links')
}
createLink(title, url, isSupplemental) {
const linkA = document.createElement('a')
linkA.href = url
linkA.innerText = title
const navigationLi = document.createElement('li')
navigationLi.appendChild(linkA)
navigationLi.title = title
if (isSupplemental) {
this.supplementalLinks.appendChild(navigationLi)
} else {
this.mainLinks.appendChild(navigationLi)
}
}
refreshSectionPolicyLinks(policy) {
if (policy.showDiagnostics) {
this.createLink('Diagnostics', '/diagnostics', true)
}
if (policy.showLogList) {
this.createLink('Logs', '/logs', true)
}
}
}

View File

@ -1,4 +1,5 @@
import './ActionButton.js' // To define action-button import './ActionButton.js' // To define action-button
import { NavigationBar } from './NavigationBar.js'
import { ExecutionDialog } from './ExecutionDialog.js' import { ExecutionDialog } from './ExecutionDialog.js'
import { ActionStatusDisplay } from './ActionStatusDisplay.js' import { ActionStatusDisplay } from './ActionStatusDisplay.js'
@ -76,6 +77,8 @@ function createAnnotation (key, val) {
* This is a weird function that just sets some globals. * This is a weird function that just sets some globals.
*/ */
export function initMarshaller () { export function initMarshaller () {
window.navbar = new NavigationBar()
window.showSection = showSection window.showSection = showSection
window.showSectionView = showSectionView window.showSectionView = showSectionView
@ -124,6 +127,10 @@ export function marshalDashboardComponentsJsonToHtml (json) {
marshalActionsJsonToHtml(json) marshalActionsJsonToHtml(json)
marshalDashboardStructureToHtml(json) marshalDashboardStructureToHtml(json)
window.navbar.refreshSectionPolicyLinks(json.effectivePolicy)
refreshDiagnostics(json)
} }
document.body.setAttribute('initial-marshal-complete', 'true') document.body.setAttribute('initial-marshal-complete', 'true')
@ -342,8 +349,8 @@ export function setupSectionNavigation (style) {
} }
registerSection('/', 'Actions', null, document.getElementById('showActions')) registerSection('/', 'Actions', null, document.getElementById('showActions'))
registerSection('/diagnostics', 'Diagnostics', null, document.getElementById('showDiagnostics')) registerSection('/diagnostics', 'Diagnostics', null, null)
registerSection('/logs', 'Logs', null, document.getElementById('showLogs')) registerSection('/logs', 'Logs', null, null)
registerSection('/login', 'Login', null, null) registerSection('/login', 'Login', null, null)
} }
@ -368,9 +375,9 @@ function addLinkToSection (pathName, element) {
} }
} }
export function refreshDiagnostics () { function refreshDiagnostics (json) {
document.getElementById('diagnostics-sshfoundkey').innerHTML = window.settings.SshFoundKey document.getElementById('diagnostics-sshfoundkey').innerHTML = json.diagnostics.SshFoundKey
document.getElementById('diagnostics-sshfoundconfig').innerHTML = window.settings.SshFoundConfig document.getElementById('diagnostics-sshfoundconfig').innerHTML = json.diagnostics.SshFoundConfig
} }
function getSystemTitle (title) { function getSystemTitle (title) {
@ -400,17 +407,11 @@ function marshalSingleDashboard (dashboard, nav) {
oldLi.remove() oldLi.remove()
} }
const navigationA = document.createElement('a') const systemTitleUrl = '/' + getSystemTitle(dashboard.title)
navigationA.title = dashboard.title
navigationA.innerText = dashboard.title
registerSection('/' + getSystemTitle(section.title), section.title, null, navigationA) window.navbar.createLink(dashboard.title, systemTitleUrl, false)
const navigationLi = document.createElement('li') registerSection(systemTitleUrl, section.title, null, null)
navigationLi.appendChild(navigationA)
navigationLi.title = dashboard.title
document.getElementById('navigation-links').appendChild(navigationLi)
} }
function marshalDashboardStructureToHtml (json) { function marshalDashboardStructureToHtml (json) {

View File

@ -6,7 +6,6 @@ import {
marshalDashboardComponentsJsonToHtml, marshalDashboardComponentsJsonToHtml,
marshalLogsJsonToHtml, marshalLogsJsonToHtml,
refreshServerConnectionLabel, refreshServerConnectionLabel,
refreshDiagnostics
} from './js/marshaller.js' } from './js/marshaller.js'
import { checkWebsocketConnection } from './js/websocket.js' import { checkWebsocketConnection } from './js/websocket.js'
@ -139,8 +138,6 @@ function processWebuiSettingsJson (settings) {
document.getElementsByTagName('main')[0].appendChild(loginForm) document.getElementsByTagName('main')[0].appendChild(loginForm)
window.settings = settings window.settings = settings
refreshDiagnostics()
} }
function processAdditionalLinks (links) { function processAdditionalLinks (links) {