- Login
-
+ Login
+
{{ username }}
-
+
@@ -71,7 +71,7 @@ import logoUrl from '../../OliveTinLogo.png';
const router = useRouter();
const sidebar = ref(null);
-const username = ref('guest');
+const username = ref('notset');
const isLoggedIn = ref(false);
const serverConnection = ref('Connected');
const currentVersion = ref('?');
@@ -84,6 +84,7 @@ const showLogs = ref(true)
const showDiagnostics = ref(true)
const initError = ref(false)
const initErrorMessage = ref('')
+const showLoginLink = ref(true)
function toggleSidebar() {
if (sidebar.value && showNavigation.value) {
@@ -102,6 +103,10 @@ function updateHeaderFromInit() {
showNavigation.value = window.initResponse.showNavigation
showLogs.value = window.initResponse.showLogList
showDiagnostics.value = window.initResponse.showDiagnostics
+
+ if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
+ showLoginLink.value = false
+ }
}
}
@@ -124,15 +129,7 @@ async function requestInit() {
window.initErrorMessage = ''
window.initCompleted = true
- username.value = initResponse.authenticatedUser
- isLoggedIn.value = initResponse.authenticatedUser !== '' && initResponse.authenticatedUser !== 'guest'
- currentVersion.value = initResponse.currentVersion
- bannerMessage.value = initResponse.bannerMessage || '';
- bannerCss.value = initResponse.bannerCss || '';
- showFooter.value = initResponse.showFooter
- showNavigation.value = initResponse.showNavigation
- showLogs.value = initResponse.showLogList
- showDiagnostics.value = initResponse.showDiagnostics
+ window.updateHeaderFromInit()
if (showNavigation.value && sidebar.value) {
for (const rootDashboard of initResponse.rootDashboards) {
diff --git a/integration-tests/proxies/haproxy/Makefile b/integration-tests/proxies/haproxy/Makefile
new file mode 100644
index 0000000..506b99b
--- /dev/null
+++ b/integration-tests/proxies/haproxy/Makefile
@@ -0,0 +1,2 @@
+make:
+ haproxy -f ./haproxy.conf
diff --git a/integration-tests/proxies/haproxy/haproxy.conf b/integration-tests/proxies/haproxy/haproxy.conf
index 0528b78..705a3a6 100644
--- a/integration-tests/proxies/haproxy/haproxy.conf
+++ b/integration-tests/proxies/haproxy/haproxy.conf
@@ -1,15 +1,32 @@
+global
+ stats socket /var/run/haproxy/admin.sock mode 660 level admin
+ log stdout local0
+
+
frontend cleartext_frontend
- bind 0.0.0.0:80
+ bind 0.0.0.0:8080
+ timeout client 10s
+
+ stats enable
+ stats uri /stats
+ stats refresh 30s
+
+ log global
option httplog
+ option dontlognull
+ log-format "[%t] %ci:%cp -> BACKEND:%b/%s | %HM %HU | Status:%ST | Bytes:%B"
+ mode http
- use_backend be_olivetin_webs if { hdr(Host) -i olivetin.example.com && path_beg /websocket }
- use_backend be_olivetin_http if { hdr(Host) -i olivetin.example.com }
+ use_backend be_olivetin if { hdr(Host) -i -m beg olivetin.example.com }
-backend be_olivetin_http
- server olivetinServer 127.0.0.1:1337 check
-
-backend be_olivetin_webs
+backend be_olivetin
+ mode http
+ timeout connect 10s
+ timeout server 10s
timeout tunnel 1h
option http-server-close
- server olivetinServer 127.0.0.1:1337
+ http-request set-header X-Forwarded-User "Alice"
+ http-request set-header X-Forwarded-Group "Group1,Group2"
+ server olivetinServer 127.0.0.1:1337 check
+
diff --git a/service/internal/acl/acl.go b/service/internal/acl/acl.go
index 59c3358..227e537 100644
--- a/service/internal/acl/acl.go
+++ b/service/internal/acl/acl.go
@@ -208,12 +208,19 @@ func UserFromContext[T any](ctx context.Context, req *connect.Request[T], cfg *c
} else {
buildUserAcls(cfg, &user)
}
+
+ path := ""
+ if req != nil {
+ path = req.Spec().Procedure
+ }
+
log.WithFields(log.Fields{
"username": user.Username,
"usergroupLine": user.UsergroupLine,
"provider": user.Provider,
"acls": user.Acls,
- }).Debugf("UserFromContext")
+ "path": path,
+ }).Debugf("Authenticated API request")
return &user
}
diff --git a/service/internal/api/api.go b/service/internal/api/api.go
index d467bcb..8eecbe9 100644
--- a/service/internal/api/api.go
+++ b/service/internal/api/api.go
@@ -481,16 +481,20 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
ret.StartOffset = page.start
return connect.NewResponse(ret), nil
}
- // Newest-first slicing: compute reversed indices
- startIdx := page.total - page.end
- endIdx := page.total - page.start
- if startIdx < 0 { startIdx = 0 }
- if endIdx > int64(len(filtered)) { endIdx = int64(len(filtered)) }
- for _, le := range filtered[startIdx:endIdx] {
- ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
- }
- // Entries older than the returned newest page
- ret.CountRemaining = page.start
+ // Newest-first slicing: compute reversed indices
+ startIdx := page.total - page.end
+ endIdx := page.total - page.start
+ if startIdx < 0 {
+ startIdx = 0
+ }
+ if endIdx > int64(len(filtered)) {
+ endIdx = int64(len(filtered))
+ }
+ for _, le := range filtered[startIdx:endIdx] {
+ ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
+ }
+ // Entries older than the returned newest page
+ ret.CountRemaining = page.start
ret.PageSize = page.size
ret.TotalCount = page.total
ret.StartOffset = page.start
@@ -662,7 +666,9 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
AuthenticatedUser: user,
}
- log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
+ log.WithFields(log.Fields{
+ "authenticatedUser": user.Username,
+ }).Debugf("EventStream: client connected")
api.streamingClientsMutex.Lock()
api.streamingClients[client] = struct{}{}
@@ -814,7 +820,7 @@ func (api *oliveTinAPI) buildRootDashboards(user *acl.AuthenticatedUser, dashboa
func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) {
defaultDashboard := buildDefaultDashboard(rr)
if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 {
- log.Infof("defaultDashboard: %+v", defaultDashboard.Contents)
+ log.Tracef("defaultDashboard: %+v", defaultDashboard.Contents)
*rootDashboards = append(*rootDashboards, "Actions")
}
}
diff --git a/service/internal/config/config.go b/service/internal/config/config.go
index 0e1e800..de0efca 100644
--- a/service/internal/config/config.go
+++ b/service/internal/config/config.go
@@ -7,212 +7,212 @@ import (
// Action represents the core functionality of OliveTin - commands that show up
// as buttons in the UI.
type Action struct {
- ID string
- Title string
- Icon string
- Shell string
- Exec []string
- ShellAfterCompleted string
- Timeout int
- Acls []string
- Entity string
- Hidden bool
- ExecOnStartup bool
- ExecOnCron []string
- ExecOnFileCreatedInDir []string
- ExecOnFileChangedInDir []string
- ExecOnCalendarFile string
- Triggers []string
- MaxConcurrent int
- MaxRate []RateSpec
- Arguments []ActionArgument
- PopupOnStart string
- SaveLogs SaveLogsConfig
+ ID string `koanf:"id"`
+ Title string `koanf:"title"`
+ Icon string `koanf:"icon"`
+ Shell string `koanf:"shell"`
+ Exec []string `koanf:"exec"`
+ ShellAfterCompleted string `koanf:"shellAfterCompleted"`
+ Timeout int `koanf:"timeout"`
+ Acls []string `koanf:"acls"`
+ Entity string `koanf:"entity"`
+ Hidden bool `koanf:"hidden"`
+ ExecOnStartup bool `koanf:"execOnStartup"`
+ ExecOnCron []string `koanf:"execOnCron"`
+ ExecOnFileCreatedInDir []string `koanf:"execOnFileCreatedInDir"`
+ ExecOnFileChangedInDir []string `koanf:"execOnFileChangedInDir"`
+ ExecOnCalendarFile string `koanf:"execOnCalendarFile"`
+ Triggers []string `koanf:"triggers"`
+ MaxConcurrent int `koanf:"maxConcurrent"`
+ MaxRate []RateSpec `koanf:"maxRate"`
+ Arguments []ActionArgument `koanf:"arguments"`
+ PopupOnStart string `koanf:"popupOnStart"`
+ SaveLogs SaveLogsConfig `koanf:"saveLogs"`
}
// ActionArgument objects appear on Actions.
type ActionArgument struct {
- Name string
- Title string
- Description string
- Type string
- Default string
- Choices []ActionArgumentChoice
- Entity string
- RejectNull bool
- Suggestions map[string]string
+ Name string `koanf:"name"`
+ Title string `koanf:"title"`
+ Description string `koanf:"description"`
+ Type string `koanf:"type"`
+ Default string `koanf:"default"`
+ Choices []ActionArgumentChoice `koanf:"choices"`
+ Entity string `koanf:"entity"`
+ RejectNull bool `koanf:"rejectNull"`
+ Suggestions map[string]string `koanf:"suggestions"`
}
// ActionArgumentChoice represents a predefined choice for an argument.
type ActionArgumentChoice struct {
- Value string
- Title string
+ Value string `koanf:"value"`
+ Title string `koanf:"title"`
}
// RateSpec allows you to set a max frequency for an action.
type RateSpec struct {
- Limit int
- Duration string
+ Limit int `koanf:"limit"`
+ Duration string `koanf:"duration"`
}
// Entity represents a "thing" that can have multiple actions associated with it.
// for example, a media player with a start and stop action.
type EntityFile struct {
- File string
- Name string
- Icon string
+ File string `koanf:"file"`
+ Name string `koanf:"name"`
+ Icon string `koanf:"icon"`
}
// PermissionsList defines what users can do with an action.
type PermissionsList struct {
- View bool `mapstructure:"view"`
- Exec bool `mapstructure:"exec"`
- Logs bool `mapstructure:"logs"`
- Kill bool `mapstructure:"kill"`
+ View bool `koanf:"view"`
+ Exec bool `koanf:"exec"`
+ Logs bool `koanf:"logs"`
+ Kill bool `koanf:"kill"`
}
// AccessControlList defines what permissions apply to a user or user group.
type AccessControlList struct {
- Name string
- AddToEveryAction bool
- MatchUsergroups []string
- MatchUsernames []string
- Permissions PermissionsList
- Policy ConfigurationPolicy
+ Name string `koanf:"name"`
+ AddToEveryAction bool `koanf:"addToEveryAction"`
+ MatchUsergroups []string `koanf:"matchUsergroups"`
+ MatchUsernames []string `koanf:"matchUsernames"`
+ Permissions PermissionsList `koanf:"permissions"`
+ Policy ConfigurationPolicy `koanf:"policy"`
}
// ConfigurationPolicy defines global settings which are overridden with an ACL.
type ConfigurationPolicy struct {
- ShowDiagnostics bool `mapstructure:"showDiagnostics"`
- ShowLogList bool `mapstructure:"showLogList"`
+ ShowDiagnostics bool `koanf:"showDiagnostics"`
+ ShowLogList bool `koanf:"showLogList"`
}
type PrometheusConfig struct {
- Enabled bool `mapstructure:"enabled"`
- DefaultGoMetrics bool `mapstructure:"defaultGoMetrics"`
+ Enabled bool `koanf:"enabled"`
+ DefaultGoMetrics bool `koanf:"defaultGoMetrics"`
}
// Config is the global config used through the whole app.
type Config struct {
- UseSingleHTTPFrontend bool `mapstructure:"useSingleHTTPFrontend"`
- ThemeName string `mapstructure:"themeName"`
- ThemeCacheDisabled bool `mapstructure:"themeCacheDisabled"`
- ListenAddressSingleHTTPFrontend string `mapstructure:"listenAddressSingleHTTPFrontend"`
- ListenAddressWebUI string `mapstructure:"listenAddressWebUI"`
- ListenAddressRestActions string `mapstructure:"listenAddressRestActions"`
- ListenAddressPrometheus string `mapstructure:"listenAddressPrometheus"`
- ExternalRestAddress string `mapstructure:"externalRestAddress"`
- LogLevel string `mapstructure:"logLevel"`
- LogDebugOptions LogDebugOptions `mapstructure:"logDebugOptions"`
- LogHistoryPageSize int64 `mapstructure:"logHistoryPageSize"`
- Actions []*Action `mapstructure:"actions"`
- Entities []*EntityFile `mapstructure:"entities"`
- Dashboards []*DashboardComponent `mapstructure:"dashboards"`
- CheckForUpdates bool `mapstructure:"checkForUpdates"`
- PageTitle string `mapstructure:"pageTitle"`
- ShowFooter bool `mapstructure:"showFooter"`
- ShowNavigation bool `mapstructure:"showNavigation"`
- ShowNewVersions bool `mapstructure:"showNewVersions"`
- EnableCustomJs bool `mapstructure:"enableCustomJs"`
- AuthJwtCookieName string `mapstructure:"authJwtCookieName"`
- AuthJwtHeader string `mapstructure:"authJwtHeader"`
- AuthJwtAud string `mapstructure:"authJwtAud"`
- AuthJwtDomain string `mapstructure:"authJwtDomain"`
- AuthJwtCertsURL string `mapstructure:"authJwtCertsUrl"`
- AuthJwtHmacSecret string `mapstructure:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
- AuthJwtClaimUsername string `mapstructure:"authJwtClaimUsername"`
- AuthJwtClaimUserGroup string `mapstructure:"authJwtClaimUserGroup"`
- AuthJwtPubKeyPath string `mapstructure:"authJwtPubKeyPath"` // will read pub key from file on disk
- AuthHttpHeaderUsername string `mapstructure:"authHttpHeaderUsername"`
- AuthHttpHeaderUserGroup string `mapstructure:"authHttpHeaderUserGroup"`
- AuthHttpHeaderUserGroupSep string `mapstructure:"authHttpHeaderUserGroupSep"`
- AuthLocalUsers AuthLocalUsersConfig `mapstructure:"authLocalUsers"`
- AuthLoginUrl string `mapstructure:"authLoginUrl"`
- AuthRequireGuestsToLogin bool `mapstructure:"authRequireGuestsToLogin"`
- AuthOAuth2RedirectURL string `mapstructure:"authOAuth2RedirectUrl"`
- AuthOAuth2Providers map[string]*OAuth2Provider `mapstructure:"authOAuth2Providers"`
- DefaultPermissions PermissionsList `mapstructure:"defaultPermissions"`
- DefaultPolicy ConfigurationPolicy `mapstructure:"defaultPolicy"`
- AccessControlLists []*AccessControlList `mapstructure:"accessControlLists"`
- WebUIDir string `mapstructure:"webUIDir"`
- CronSupportForSeconds bool `mapstructure:"cronSupportForSeconds"`
- SectionNavigationStyle string `mapstructure:"sectionNavigationStyle"`
- DefaultPopupOnStart string `mapstructure:"defaultPopupOnStart"`
- InsecureAllowDumpOAuth2UserData bool `mapstructure:"insecureAllowDumpOAuth2UserData"`
- InsecureAllowDumpVars bool `mapstructure:"insecureAllowDumpVars"`
- InsecureAllowDumpSos bool `mapstructure:"insecureAllowDumpSos"`
- InsecureAllowDumpActionMap bool `mapstructure:"insecureAllowDumpActionMap"`
- InsecureAllowDumpJwtClaims bool `mapstructure:"insecureAllowDumpJwtClaims"`
- Prometheus PrometheusConfig `mapstructure:"prometheus"`
- SaveLogs SaveLogsConfig `mapstructure:"saveLogs"`
- DefaultIconForActions string `mapstructure:"defaultIconForActions"`
- DefaultIconForDirectories string `mapstructure:"defaultIconForDirectories"`
- DefaultIconForBack string `mapstructure:"defaultIconForBack"`
- AdditionalNavigationLinks []*NavigationLink `mapstructure:"additionalNavigationLinks"`
- ServiceHostMode string `mapstructure:"serviceHostMode"`
- StyleMods []string `mapstructure:"styleMods"`
- BannerMessage string `mapstructure:"bannerMessage"`
- BannerCSS string `mapstructure:"bannerCss"`
- Include string `mapstructure:"include"`
+ UseSingleHTTPFrontend bool `koanf:"useSingleHTTPFrontend"`
+ ThemeName string `koanf:"themeName"`
+ ThemeCacheDisabled bool `koanf:"themeCacheDisabled"`
+ ListenAddressSingleHTTPFrontend string `koanf:"listenAddressSingleHTTPFrontend"`
+ ListenAddressWebUI string `koanf:"listenAddressWebUI"`
+ ListenAddressRestActions string `koanf:"listenAddressRestActions"`
+ ListenAddressPrometheus string `koanf:"listenAddressPrometheus"`
+ ExternalRestAddress string `koanf:"externalRestAddress"`
+ LogLevel string `koanf:"logLevel"`
+ LogDebugOptions LogDebugOptions `koanf:"logDebugOptions"`
+ LogHistoryPageSize int64 `koanf:"logHistoryPageSize"`
+ Actions []*Action `koanf:"actions"`
+ Entities []*EntityFile `koanf:"entities"`
+ Dashboards []*DashboardComponent `koanf:"dashboards"`
+ CheckForUpdates bool `koanf:"checkForUpdates"`
+ PageTitle string `koanf:"pageTitle"`
+ ShowFooter bool `koanf:"showFooter"`
+ ShowNavigation bool `koanf:"showNavigation"`
+ ShowNewVersions bool `koanf:"showNewVersions"`
+ EnableCustomJs bool `koanf:"enableCustomJs"`
+ AuthJwtCookieName string `koanf:"authJwtCookieName"`
+ AuthJwtHeader string `koanf:"authJwtHeader"`
+ AuthJwtAud string `koanf:"authJwtAud"`
+ AuthJwtDomain string `koanf:"authJwtDomain"`
+ AuthJwtCertsURL string `koanf:"authJwtCertsUrl"`
+ AuthJwtHmacSecret string `koanf:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
+ AuthJwtClaimUsername string `koanf:"authJwtClaimUsername"`
+ AuthJwtClaimUserGroup string `koanf:"authJwtClaimUserGroup"`
+ AuthJwtPubKeyPath string `koanf:"authJwtPubKeyPath"` // will read pub key from file on disk
+ AuthHttpHeaderUsername string `koanf:"authHttpHeaderUsername"`
+ AuthHttpHeaderUserGroup string `koanf:"authHttpHeaderUserGroup"`
+ AuthHttpHeaderUserGroupSep string `koanf:"authHttpHeaderUserGroupSep"`
+ AuthLocalUsers AuthLocalUsersConfig `koanf:"authLocalUsers"`
+ AuthLoginUrl string `koanf:"authLoginUrl"`
+ AuthRequireGuestsToLogin bool `koanf:"authRequireGuestsToLogin"`
+ AuthOAuth2RedirectURL string `koanf:"authOAuth2RedirectUrl"`
+ AuthOAuth2Providers map[string]*OAuth2Provider `koanf:"authOAuth2Providers"`
+ DefaultPermissions PermissionsList `koanf:"defaultPermissions"`
+ DefaultPolicy ConfigurationPolicy `koanf:"defaultPolicy"`
+ AccessControlLists []*AccessControlList `koanf:"accessControlLists"`
+ WebUIDir string `koanf:"webUIDir"`
+ CronSupportForSeconds bool `koanf:"cronSupportForSeconds"`
+ SectionNavigationStyle string `koanf:"sectionNavigationStyle"`
+ DefaultPopupOnStart string `koanf:"defaultPopupOnStart"`
+ InsecureAllowDumpOAuth2UserData bool `koanf:"insecureAllowDumpOAuth2UserData"`
+ InsecureAllowDumpVars bool `koanf:"insecureAllowDumpVars"`
+ InsecureAllowDumpSos bool `koanf:"insecureAllowDumpSos"`
+ InsecureAllowDumpActionMap bool `koanf:"insecureAllowDumpActionMap"`
+ InsecureAllowDumpJwtClaims bool `koanf:"insecureAllowDumpJwtClaims"`
+ Prometheus PrometheusConfig `koanf:"prometheus"`
+ SaveLogs SaveLogsConfig `koanf:"saveLogs"`
+ DefaultIconForActions string `koanf:"defaultIconForActions"`
+ DefaultIconForDirectories string `koanf:"defaultIconForDirectories"`
+ DefaultIconForBack string `koanf:"defaultIconForBack"`
+ AdditionalNavigationLinks []*NavigationLink `koanf:"additionalNavigationLinks"`
+ ServiceHostMode string `koanf:"serviceHostMode"`
+ StyleMods []string `koanf:"styleMods"`
+ BannerMessage string `koanf:"bannerMessage"`
+ BannerCSS string `koanf:"bannerCss"`
+ Include string `koanf:"include"`
sourceFiles []string
}
type AuthLocalUsersConfig struct {
- Enabled bool `mapstructure:"enabled"`
- Users []*LocalUser `mapstructure:"users"`
+ Enabled bool `koanf:"enabled"`
+ Users []*LocalUser `koanf:"users"`
}
type LocalUser struct {
- Username string `mapstructure:"username"`
- Usergroup string `mapstructure:"usergroup"`
- Password string `mapstructure:"password"`
+ Username string `koanf:"username"`
+ Usergroup string `koanf:"usergroup"`
+ Password string `koanf:"password"`
}
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
+ Name string `koanf:"name"`
+ Title string `koanf:"title"`
+ ClientID string `koanf:"clientId"`
+ ClientSecret string `koanf:"clientSecret"`
+ Icon string `koanf:"icon"`
+ Scopes []string `koanf:"scopes"`
+ AuthUrl string `koanf:"authUrl"`
+ TokenUrl string `koanf:"tokenUrl"`
+ WhoamiUrl string `koanf:"whoamiUrl"`
+ UsernameField string `koanf:"usernameField"`
+ UserGroupField string `koanf:"userGroupField"`
+ InsecureSkipVerify bool `koanf:"insecureSkipVerify"`
+ CallbackTimeout int `koanf:"callbackTimeout"`
+ CertBundlePath string `koanf:"certBundlePath"`
}
type NavigationLink struct {
- Title string `mapstructure:"title"`
- Url string `mapstructure:"url"`
- Target string `mapstructure:"target"`
+ Title string `koanf:"title"`
+ Url string `koanf:"url"`
+ Target string `koanf:"target"`
}
type SaveLogsConfig struct {
- ResultsDirectory string `mapstructure:"resultsDirectory"`
- OutputDirectory string `mapstructure:"outputDirectory"`
+ ResultsDirectory string `koanf:"resultsDirectory"`
+ OutputDirectory string `koanf:"outputDirectory"`
}
type LogDebugOptions struct {
- SingleFrontendRequests bool
- SingleFrontendRequestHeaders bool
- AclCheckStarted bool
- AclMatched bool
- AclNotMatched bool
- AclNoneMatched bool
+ SingleFrontendRequests bool `koanf:"singleFrontendRequests"`
+ SingleFrontendRequestHeaders bool `koanf:"singleFrontendRequestHeaders"`
+ AclCheckStarted bool `koanf:"aclCheckStarted"`
+ AclMatched bool `koanf:"aclMatched"`
+ AclNotMatched bool `koanf:"aclNotMatched"`
+ AclNoneMatched bool `koanf:"aclNoneMatched"`
}
type DashboardComponent struct {
- Title string
- Type string
- Entity string
- Icon string
- CssClass string
- Contents []*DashboardComponent
+ Title string `koanf:"title"`
+ Type string `koanf:"type"`
+ Entity string `koanf:"entity"`
+ Icon string `koanf:"icon"`
+ CssClass string `koanf:"cssClass"`
+ Contents []*DashboardComponent `koanf:"contents"`
}
func DefaultConfig() *Config {
diff --git a/service/internal/config/config_reloader.go b/service/internal/config/config_reloader.go
index ab8e5d7..9909f02 100644
--- a/service/internal/config/config_reloader.go
+++ b/service/internal/config/config_reloader.go
@@ -34,90 +34,32 @@ func AddListener(l func()) {
listeners = append(listeners, l)
}
-// AppendSourceWithIncludes loads base config and any included configs
-func AppendSourceWithIncludes(cfg *Config, k *koanf.Koanf, configPath string) {
- // Load base config first
- AppendSource(cfg, k, configPath)
-
- // Load included configs if specified
- if cfg.Include != "" {
- LoadIncludedConfigs(cfg, k, configPath)
- }
-}
-
func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
- log.Infof("Appending cfg source: %s", configPath)
+ log.WithFields(log.Fields{
+ "configPath": configPath,
+ }).Info("Appending cfg source")
+
+ loadIncludedConfigsFromDir(k, configPath)
if !unmarshalRoot(k, cfg) {
return
}
- loadCollectionsFallbacks(k, cfg)
-
- applyConfigOverrides(k, cfg)
-
afterLoadFinalize(cfg, configPath)
}
func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool {
- if err := k.Unmarshal(".", cfg); err != nil {
+ err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{
+ Tag: "koanf",
+ })
+
+ if err != nil {
log.Errorf("Error unmarshalling config: %v", err)
return false
}
return true
}
-func loadCollectionsFallbacks(k *koanf.Koanf, cfg *Config) {
- maybeUnmarshalActions(k, cfg)
- maybeUnmarshalDashboards(k, cfg)
- maybeUnmarshalEntities(k, cfg)
- maybeUnmarshalAuthLocalUsers(k, cfg)
-}
-
-func maybeUnmarshalActions(k *koanf.Koanf, cfg *Config) {
- if len(cfg.Actions) != 0 || !k.Exists("actions") {
- return
- }
- var actions []*Action
- if err := k.Unmarshal("actions", &actions); err == nil {
- cfg.Actions = actions
- log.Debugf("Manually loaded %d actions", len(actions))
- }
-}
-
-func maybeUnmarshalDashboards(k *koanf.Koanf, cfg *Config) {
- if len(cfg.Dashboards) != 0 || !k.Exists("dashboards") {
- return
- }
- var dashboards []*DashboardComponent
- if err := k.Unmarshal("dashboards", &dashboards); err == nil {
- cfg.Dashboards = dashboards
- log.Debugf("Manually loaded %d dashboards", len(dashboards))
- }
-}
-
-func maybeUnmarshalEntities(k *koanf.Koanf, cfg *Config) {
- if len(cfg.Entities) != 0 || !k.Exists("entities") {
- return
- }
- var entities []*EntityFile
- if err := k.Unmarshal("entities", &entities); err == nil {
- cfg.Entities = entities
- log.Debugf("Manually loaded %d entities", len(entities))
- }
-}
-
-func maybeUnmarshalAuthLocalUsers(k *koanf.Koanf, cfg *Config) {
- if len(cfg.AuthLocalUsers.Users) != 0 || !k.Exists("authLocalUsers") {
- return
- }
- var authLocalUsers AuthLocalUsersConfig
- if err := k.Unmarshal("authLocalUsers", &authLocalUsers); err == nil {
- cfg.AuthLocalUsers = authLocalUsers
- log.Debugf("Manually loaded local auth config")
- }
-}
-
func afterLoadFinalize(cfg *Config, configPath string) {
metricConfigReloadedCount.Inc()
metricConfigActionCount.Set(float64(len(cfg.Actions)))
@@ -130,39 +72,19 @@ func afterLoadFinalize(cfg *Config, configPath string) {
}
}
-func applyConfigOverrides(k *koanf.Koanf, cfg *Config) {
- // Override fields that should be read from config
- // mapstructure tags should make most of this unnecessary, but keep for safety
- boolVal(k, "showFooter", &cfg.ShowFooter)
- boolVal(k, "showNavigation", &cfg.ShowNavigation)
- boolVal(k, "checkForUpdates", &cfg.CheckForUpdates)
- boolVal(k, "useSingleHTTPFrontend", &cfg.UseSingleHTTPFrontend)
- stringVal(k, "logLevel", &cfg.LogLevel)
- stringVal(k, "pageTitle", &cfg.PageTitle)
- boolVal(k, "authRequireGuestsToLogin", &cfg.AuthRequireGuestsToLogin)
- stringVal(k, "include", &cfg.Include)
+// loadIncludedConfigsFromDir loads configuration files from an include directory and merges them
+func loadIncludedConfigsFromDir(k *koanf.Koanf, baseConfigPath string) {
+ relativeIncludePath := k.String("include")
- // Handle nested defaultPolicy struct
- if k.Exists("defaultPolicy") {
- boolVal(k, "defaultPolicy.showDiagnostics", &cfg.DefaultPolicy.ShowDiagnostics)
- boolVal(k, "defaultPolicy.showLogList", &cfg.DefaultPolicy.ShowLogList)
- }
-
- // Handle nested prometheus struct
- if k.Exists("prometheus") {
- boolVal(k, "prometheus.enabled", &cfg.Prometheus.Enabled)
- boolVal(k, "prometheus.defaultGoMetrics", &cfg.Prometheus.DefaultGoMetrics)
- }
-}
-
-// LoadIncludedConfigs loads configuration files from an include directory and merges them
-func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
- if cfg.Include == "" {
+ if relativeIncludePath == "" {
return
}
- includePath := filepath.Join(filepath.Dir(baseConfigPath), cfg.Include)
- log.Infof("Loading included configs from: %s", includePath)
+ includePath := filepath.Join(filepath.Dir(baseConfigPath), relativeIncludePath)
+
+ log.WithFields(log.Fields{
+ "includePath": includePath,
+ }).Infof("Loading included configs from dir")
yamlFiles, ok := listYamlFiles(includePath)
if !ok || len(yamlFiles) == 0 {
@@ -171,11 +93,10 @@ func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
sort.Strings(yamlFiles)
for _, filename := range yamlFiles {
- loadAndMergeIncludedFile(cfg, includePath, filename)
+ loadAndMergeIncludedFile(k, includePath, filename)
}
log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
- cfg.Sanitize()
}
func listYamlFiles(includePath string) ([]string, bool) {
@@ -209,152 +130,45 @@ func listYamlFiles(includePath string) ([]string, bool) {
return yamlFiles, true
}
-func loadAndMergeIncludedFile(cfg *Config, includePath, filename string) {
+func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
filePath := filepath.Join(includePath, filename)
- log.Infof("Loading included config file: %s", filePath)
- includeK := koanf.New(".")
- if err := includeK.Load(file.Provider(filePath), yaml.Parser()); err != nil {
+ if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil {
log.Errorf("Error loading included config file %s: %v", filePath, err)
return
}
- tempCfg := &Config{}
- if err := includeK.Unmarshal(".", tempCfg); err != nil {
- log.Errorf("Error unmarshalling included config file %s: %v", filePath, err)
- return
- }
-
- loadCollectionsFallbacks(includeK, tempCfg)
-
- mergeConfig(cfg, tempCfg)
- log.Infof("Successfully loaded and merged %s", filename)
+ log.WithFields(log.Fields{
+ "filePath": filePath,
+ }).Info("Successfully loaded included config file")
}
-func mergeConfig(base *Config, overlay *Config) {
- mergeSlices(base, overlay)
- overrideSimple(base, overlay)
- overrideNested(base, overlay)
- overrideStrings(base, overlay)
-}
+func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
+ // Handle actions merging - koanf provides []interface{} not []*Action
+ // Merge src (new) into dest (existing) by appending src's actions to dest's actions
+ if srcActions, ok := src["actions"]; ok {
+ if destActions, ok := dest["actions"]; ok {
+ // Both have actions - append src to dest
+ srcSlice, ok1 := srcActions.([]interface{})
+ destSlice, ok2 := destActions.([]interface{})
+ if ok1 && ok2 {
+ dest["actions"] = append(destSlice, srcSlice...)
+ } else {
+ // Fallback: if types don't match, just use src
+ dest["actions"] = srcActions
+ }
+ } else {
+ // dest doesn't have actions, so use src's actions
+ dest["actions"] = srcActions
+ }
+ }
+ // If src doesn't have actions, leave dest unchanged
-func mergeSlices(base *Config, overlay *Config) {
- if len(overlay.Actions) > 0 {
- base.Actions = append(base.Actions, overlay.Actions...)
- }
- if len(overlay.Dashboards) > 0 {
- base.Dashboards = append(base.Dashboards, overlay.Dashboards...)
- log.Debugf("Merged %d dashboards from include", len(overlay.Dashboards))
- }
- if len(overlay.Entities) > 0 {
- base.Entities = append(base.Entities, overlay.Entities...)
- log.Debugf("Merged %d entities from include", len(overlay.Entities))
- }
- if len(overlay.AccessControlLists) > 0 {
- base.AccessControlLists = append(base.AccessControlLists, overlay.AccessControlLists...)
- log.Debugf("Merged %d access control lists from include", len(overlay.AccessControlLists))
- }
- if len(overlay.AuthLocalUsers.Users) > 0 {
- base.AuthLocalUsers.Users = append(base.AuthLocalUsers.Users, overlay.AuthLocalUsers.Users...)
- log.Debugf("Merged %d local users from include", len(overlay.AuthLocalUsers.Users))
- }
- if len(overlay.StyleMods) > 0 {
- base.StyleMods = append(base.StyleMods, overlay.StyleMods...)
- }
- if len(overlay.AdditionalNavigationLinks) > 0 {
- base.AdditionalNavigationLinks = append(base.AdditionalNavigationLinks, overlay.AdditionalNavigationLinks...)
- }
-}
-
-func overrideSimple(base *Config, overlay *Config) {
- if overlay.LogLevel != "" {
- base.LogLevel = overlay.LogLevel
- }
- if overlay.PageTitle != "" {
- base.PageTitle = overlay.PageTitle
- }
- if overlay.ShowFooter != base.ShowFooter {
- base.ShowFooter = overlay.ShowFooter
- }
- if overlay.ShowNavigation != base.ShowNavigation {
- base.ShowNavigation = overlay.ShowNavigation
- }
- if overlay.CheckForUpdates != base.CheckForUpdates {
- base.CheckForUpdates = overlay.CheckForUpdates
- }
- if overlay.UseSingleHTTPFrontend != base.UseSingleHTTPFrontend {
- base.UseSingleHTTPFrontend = overlay.UseSingleHTTPFrontend
- }
- if overlay.AuthRequireGuestsToLogin != base.AuthRequireGuestsToLogin {
- base.AuthRequireGuestsToLogin = overlay.AuthRequireGuestsToLogin
- }
- if overlay.AuthLocalUsers.Enabled {
- base.AuthLocalUsers.Enabled = overlay.AuthLocalUsers.Enabled
- }
-}
-
-func overrideNested(base *Config, overlay *Config) {
- // Only apply overrides when overlay explicitly enables the option.
- // This mirrors the presence-check pattern used elsewhere to avoid
- // unintentionally disabling an already-enabled base setting with a default false.
- if overlay.DefaultPolicy.ShowDiagnostics {
- base.DefaultPolicy.ShowDiagnostics = true
- }
- if overlay.DefaultPolicy.ShowLogList {
- base.DefaultPolicy.ShowLogList = true
- }
- if overlay.Prometheus.Enabled {
- base.Prometheus.Enabled = true
- }
- if overlay.Prometheus.DefaultGoMetrics {
- base.Prometheus.DefaultGoMetrics = true
- }
-}
-
-func overrideStrings(base *Config, overlay *Config) {
- overrideString(&base.BannerMessage, overlay.BannerMessage)
- overrideString(&base.BannerCSS, overlay.BannerCSS)
- overrideString(&base.LogLevel, overlay.LogLevel)
- overrideString(&base.PageTitle, overlay.PageTitle)
- overrideString(&base.SectionNavigationStyle, overlay.SectionNavigationStyle)
- overrideString(&base.DefaultPopupOnStart, overlay.DefaultPopupOnStart)
-}
-
-func overrideString(base *string, overlay string) {
- if overlay != "" {
- *base = overlay
- }
-}
-
-func getActionTitles(actions []*Action) []string {
- titles := make([]string, len(actions))
- for i, action := range actions {
- titles[i] = action.Title
- }
- return titles
+ return nil
}
var envRegex = regexp.MustCompile(`\${{ *?(\S+) *?}}`)
-// Helper functions to reduce repetitive if/set chains
-func stringVal(k *koanf.Koanf, key string, dest *string) {
- if k.Exists(key) {
- *dest = k.String(key)
- }
-}
-
-func boolVal(k *koanf.Koanf, key string, dest *bool) {
- if k.Exists(key) {
- *dest = k.Bool(key)
- }
-}
-
-func int64Val(k *koanf.Koanf, key string, dest *int64) {
- if k.Exists(key) {
- *dest = k.Int64(key)
- }
-}
-
func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) {
log.Debugf("envDecodeHookFunc called: from=%v, to=%v, data=%v", from, to, data)
if from.Kind() != reflect.String {
diff --git a/service/internal/executor/executor_actions.go b/service/internal/executor/executor_actions.go
index 36327ea..053af80 100644
--- a/service/internal/executor/executor_actions.go
+++ b/service/internal/executor/executor_actions.go
@@ -53,7 +53,9 @@ func (e *Executor) RebuildActionMap() {
findDashboardActionTitles(req)
- log.Infof("dashboardActionTitles: %v", req.DashboardActionTitles)
+ log.WithFields(log.Fields{
+ "titles": req.DashboardActionTitles,
+ }).Trace("dashboardActionTitles")
for configOrder, action := range e.Cfg.Actions {
if action.Entity != "" {
diff --git a/service/internal/httpservers/singleFrontend.go b/service/internal/httpservers/singleFrontend.go
index e7627e6..e42e493 100644
--- a/service/internal/httpservers/singleFrontend.go
+++ b/service/internal/httpservers/singleFrontend.go
@@ -53,7 +53,11 @@ func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
r.URL.Path = apiPath + fn
- log.Debugf("SingleFrontend HTTP API Req URL after rewrite: %v", r.URL.Path)
+ log.WithFields(log.Fields{
+ "path": r.URL.Path,
+ }).Tracef("SingleFrontend HTTP API Req URL after rewrite")
+
+ logDebugRequest(cfg, "api", r)
apiHandler.ServeHTTP(w, r)
}))
diff --git a/service/internal/httpservers/webuiServer.go b/service/internal/httpservers/webuiServer.go
index 86ba263..7a0aa8f 100644
--- a/service/internal/httpservers/webuiServer.go
+++ b/service/internal/httpservers/webuiServer.go
@@ -45,7 +45,7 @@ func (s *webUIServer) handleWebui(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path.Join(s.webuiDir, "index.html"))
} else {
- log.Infof("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
+ log.Tracef("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
http.ServeFile(w, r, path.Join(s.webuiDir, r.URL.Path))
// http.StripPrefix(dirName, http.FileServer(http.Dir(s.webuiDir))).ServeHTTP(w, r)
}
diff --git a/service/internal/websocket/websocket.go b/service/internal/websocket/websocket.go
deleted file mode 100644
index f489988..0000000
--- a/service/internal/websocket/websocket.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package websocket
-
-import (
- "net/http"
- "sync"
-
- apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
- "github.com/OliveTin/OliveTin/internal/executor"
- ws "github.com/gorilla/websocket"
- log "github.com/sirupsen/logrus"
- "google.golang.org/protobuf/encoding/protojson"
- "google.golang.org/protobuf/reflect/protoreflect"
-)
-
-var upgrader = ws.Upgrader{
- CheckOrigin: checkOriginPermissive,
-}
-
-var (
- sendmutex = sync.Mutex{}
-)
-
-type WebsocketClient struct {
- conn *ws.Conn
-}
-
-var clients []*WebsocketClient
-
-var marshalOptions = protojson.MarshalOptions{
- UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
- EmitUnpopulated: true,
-}
-
-var ExecutionListener WebsocketExecutionListener
-
-type WebsocketExecutionListener struct{}
-
-func (WebsocketExecutionListener) OnExecutionStarted(ile *executor.InternalLogEntry) {
- broadcast(&apiv1.EventExecutionStarted{
- LogEntry: internalLogEntryToPb(ile),
- })
-}
-
-func OnEntityChanged() {
- broadcast(&apiv1.EventEntityChanged{})
-}
-
-func (WebsocketExecutionListener) OnActionMapRebuilt() {
- broadcast(&apiv1.EventConfigChanged{})
-}
-
-/*
-The default checkOrigin function checks that the origin (browser) matches the
-request origin. However in OliveTin we expect many users to deliberately proxy
-the connection with reverse proxies.
-
-So, we just permit any origin. After some searching I'm not sure if this exposes
-OliveTin to security issues, but it seems probably not. It would be possible to
-create a config option like PermitWebsocketConnectionsFrom or something, but
-I'd prefer if OliveTin works as much as possible "out of the box".
-
-If this does expose OliveTin to security issues, it will be changed in the
-future obviously.
-*/
-func checkOriginPermissive(r *http.Request) bool {
- return true
-}
-
-func (WebsocketExecutionListener) OnOutputChunk(chunk []byte, executionTrackingId string) {
- log.Tracef("outputchunk: %s", string(chunk))
-
- oc := &apiv1.EventOutputChunk{
- Output: string(chunk),
- ExecutionTrackingId: executionTrackingId,
- }
-
- broadcast(oc)
-}
-
-func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
- evt := &apiv1.EventExecutionFinished{
- LogEntry: internalLogEntryToPb(logEntry),
- }
-
- log.Infof("WS Execution finished: %v", evt.LogEntry)
-
- broadcast(evt)
-}
-
-func broadcast(pbmsg protoreflect.ProtoMessage) {
- payload, err := marshalOptions.Marshal(pbmsg)
-
- if err != nil {
- log.Errorf("websocket marshal error: %v", err)
- return
- }
-
- messageType := pbmsg.ProtoReflect().Descriptor().FullName()
-
- //