diff --git a/integration-tests/configs/policy-all-false/config.yaml b/integration-tests/configs/policy-all-false/config.yaml new file mode 100644 index 0000000..3fd7c86 --- /dev/null +++ b/integration-tests/configs/policy-all-false/config.yaml @@ -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: "🥱" diff --git a/integration-tests/test/general.mjs b/integration-tests/test/general.mjs index c3895f0..36cb37b 100644 --- a/integration-tests/test/general.mjs +++ b/integration-tests/test/general.mjs @@ -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 () { const ftr = await webdriver.findElement(By.tagName('footer')).getText() diff --git a/integration-tests/test/policy-all-false.mjs b/integration-tests/test/policy-all-false.mjs new file mode 100644 index 0000000..10d42b5 --- /dev/null +++ b/integration-tests/test/policy-all-false.mjs @@ -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 + }) +}) diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index 8660450..e28c2d7 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -49,6 +49,19 @@ message GetDashboardComponentsResponse { string authenticated_user = 5; 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 {} diff --git a/service/internal/acl/acl.go b/service/internal/acl/acl.go index f1fa32c..e91b9f8 100644 --- a/service/internal/acl/acl.go +++ b/service/internal/acl/acl.go @@ -32,6 +32,8 @@ type AuthenticatedUser struct { SID string Acls []string + + EffectivePolicy *config.ConfigurationPolicy } func (u *AuthenticatedUser) IsGuest() bool { @@ -43,15 +45,22 @@ func logAclNotMatched(cfg *config.Config, aclFunction string, user *Authenticate log.WithFields(log.Fields{ "User": user.Username, "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) { + actionTitle := "N/A" + + if action != nil { + actionTitle = action.Title + } + if cfg.LogDebugOptions.AclMatched { log.WithFields(log.Fields{ "User": user.Username, - "Action": action.Title, + "Action": actionTitle, "ACL": acl.Name, }).Debugf("%v - Matched ACL", aclFunction) } @@ -209,6 +218,8 @@ func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) { continue } } + + user.EffectivePolicy = getEffectivePolicy(cfg, user) } func hasGroupsMatch(matchUsergroups []string, usergroup string) bool { @@ -249,3 +260,32 @@ func getRelevantAcls(cfg *config.Config, actionAcls []string, user *Authenticate return ret } + +func getEffectivePolicy(cfg *config.Config, user *AuthenticatedUser) *config.ConfigurationPolicy { + ret := &config.ConfigurationPolicy{ + ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics, + ShowLogList: cfg.DefaultPolicy.ShowLogList, + } + + for _, acl := range cfg.AccessControlLists { + if slices.Contains(user.Acls, acl.Name) { + logAclMatched(cfg, "GetEffectivePolicy", user, nil, acl) + + ret = buildConfigurationPolicy(ret, acl.Policy) + } + } + + return ret +} + +func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy { + if policy.ShowDiagnostics { + ret.ShowDiagnostics = policy.ShowDiagnostics + } + + if policy.ShowLogList { + ret.ShowLogList = policy.ShowLogList + } + + return ret +} diff --git a/service/internal/config/config.go b/service/internal/config/config.go index fca1ea0..ad1c818 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -76,6 +76,13 @@ type AccessControlList struct { MatchUsergroups []string MatchUsernames []string Permissions PermissionsList + Policy ConfigurationPolicy +} + +// ConfigurationPolicy defines global settings which are overridden with an ACL. +type ConfigurationPolicy struct { + ShowDiagnostics bool + ShowLogList bool } type PrometheusConfig struct { @@ -123,6 +130,7 @@ type Config struct { AuthOAuth2RedirectURL string AuthOAuth2Providers map[string]*OAuth2Provider DefaultPermissions PermissionsList + DefaultPolicy ConfigurationPolicy AccessControlLists []*AccessControlList WebUIDir string CronSupportForSeconds bool @@ -243,5 +251,8 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.ListenAddressWebUI = fmt.Sprintf("localhost:%d", basePort+3) config.ListenAddressPrometheus = fmt.Sprintf("localhost:%d", basePort+4) + config.DefaultPolicy.ShowDiagnostics = true + config.DefaultPolicy.ShowLogList = true + return &config } diff --git a/service/internal/grpcapi/grpcApiActions.go b/service/internal/grpcapi/grpcApiActions.go index 3e77463..6f4898c 100644 --- a/service/internal/grpcapi/grpcApiActions.go +++ b/service/internal/grpcapi/grpcApiActions.go @@ -5,6 +5,7 @@ import ( acl "github.com/OliveTin/OliveTin/internal/acl" config "github.com/OliveTin/OliveTin/internal/config" executor "github.com/OliveTin/OliveTin/internal/executor" + installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo" sv "github.com/OliveTin/OliveTin/internal/stringvariables" "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 } +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 { action := actionBinding.Action diff --git a/service/internal/httpservers/webuiServer.go b/service/internal/httpservers/webuiServer.go index 53c96f3..3beb423 100644 --- a/service/internal/httpservers/webuiServer.go +++ b/service/internal/httpservers/webuiServer.go @@ -29,8 +29,6 @@ type webUISettings struct { PageTitle string SectionNavigationStyle string DefaultIconForBack string - SshFoundKey string - SshFoundConfig string EnableCustomJs bool AuthLoginUrl string AuthLocalLogin bool @@ -138,8 +136,6 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) { 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, diff --git a/webui.dev/index.html b/webui.dev/index.html index d8c4245..b9a4647 100644 --- a/webui.dev/index.html +++ b/webui.dev/index.html @@ -35,12 +35,6 @@ diff --git a/webui.dev/js/NavigationBar.js b/webui.dev/js/NavigationBar.js new file mode 100644 index 0000000..5400f28 --- /dev/null +++ b/webui.dev/js/NavigationBar.js @@ -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) + } + } +} diff --git a/webui.dev/js/marshaller.js b/webui.dev/js/marshaller.js index 293bc82..9be7375 100644 --- a/webui.dev/js/marshaller.js +++ b/webui.dev/js/marshaller.js @@ -1,4 +1,5 @@ import './ActionButton.js' // To define action-button +import { NavigationBar } from './NavigationBar.js' import { ExecutionDialog } from './ExecutionDialog.js' import { ActionStatusDisplay } from './ActionStatusDisplay.js' @@ -76,6 +77,8 @@ function createAnnotation (key, val) { * This is a weird function that just sets some globals. */ export function initMarshaller () { + window.navbar = new NavigationBar() + window.showSection = showSection window.showSectionView = showSectionView @@ -124,6 +127,10 @@ export function marshalDashboardComponentsJsonToHtml (json) { marshalActionsJsonToHtml(json) marshalDashboardStructureToHtml(json) + + window.navbar.refreshSectionPolicyLinks(json.effectivePolicy) + + refreshDiagnostics(json) } document.body.setAttribute('initial-marshal-complete', 'true') @@ -342,8 +349,8 @@ export function setupSectionNavigation (style) { } registerSection('/', 'Actions', null, document.getElementById('showActions')) - registerSection('/diagnostics', 'Diagnostics', null, document.getElementById('showDiagnostics')) - registerSection('/logs', 'Logs', null, document.getElementById('showLogs')) + registerSection('/diagnostics', 'Diagnostics', null, null) + registerSection('/logs', 'Logs', null, null) registerSection('/login', 'Login', null, null) } @@ -368,9 +375,9 @@ function addLinkToSection (pathName, element) { } } -export function refreshDiagnostics () { - document.getElementById('diagnostics-sshfoundkey').innerHTML = window.settings.SshFoundKey - document.getElementById('diagnostics-sshfoundconfig').innerHTML = window.settings.SshFoundConfig +function refreshDiagnostics (json) { + document.getElementById('diagnostics-sshfoundkey').innerHTML = json.diagnostics.SshFoundKey + document.getElementById('diagnostics-sshfoundconfig').innerHTML = json.diagnostics.SshFoundConfig } function getSystemTitle (title) { @@ -400,17 +407,11 @@ function marshalSingleDashboard (dashboard, nav) { oldLi.remove() } - const navigationA = document.createElement('a') - navigationA.title = dashboard.title - navigationA.innerText = dashboard.title + const systemTitleUrl = '/' + getSystemTitle(dashboard.title) - registerSection('/' + getSystemTitle(section.title), section.title, null, navigationA) + window.navbar.createLink(dashboard.title, systemTitleUrl, false) - const navigationLi = document.createElement('li') - navigationLi.appendChild(navigationA) - navigationLi.title = dashboard.title - - document.getElementById('navigation-links').appendChild(navigationLi) + registerSection(systemTitleUrl, section.title, null, null) } function marshalDashboardStructureToHtml (json) { diff --git a/webui.dev/main.js b/webui.dev/main.js index 2594680..4bbd01b 100644 --- a/webui.dev/main.js +++ b/webui.dev/main.js @@ -6,7 +6,6 @@ import { marshalDashboardComponentsJsonToHtml, marshalLogsJsonToHtml, refreshServerConnectionLabel, - refreshDiagnostics } from './js/marshaller.js' import { checkWebsocketConnection } from './js/websocket.js' @@ -139,8 +138,6 @@ function processWebuiSettingsJson (settings) { document.getElementsByTagName('main')[0].appendChild(loginForm) window.settings = settings - - refreshDiagnostics() } function processAdditionalLinks (links) {