olivetin/service/internal/config/sanitize.go

348 lines
8.2 KiB
Go

package config
import (
"fmt"
"strings"
"text/template"
"github.com/OliveTin/OliveTin/internal/env"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
// Sanitize will look for common configuration issues, and fix them. For example,
// populating undefined fields - name -> title, etc.
func (cfg *Config) Sanitize() {
cfg.sanitizeLogLevel()
cfg.sanitizeAuthRequireGuestsToLogin()
cfg.sanitizeLogHistoryPageSize()
cfg.sanitizeLocalUsers()
cfg.sanitizeSecurityHeaders()
// log.Infof("cfg %p", cfg)
for idx := range cfg.Actions {
cfg.Actions[idx].sanitize(cfg)
}
cfg.sanitizeDashboardsForInlineActions()
if err := cfg.validateReservedActionArgumentNames(); err != nil {
log.Fatalf("%v", err)
}
}
func (cfg *Config) validateReservedActionArgumentNames() error {
for _, action := range cfg.Actions {
if err := action.validateReservedArgumentNames(); err != nil {
return err
}
}
return nil
}
func (action *Action) validateReservedArgumentNames() error {
if action == nil {
return nil
}
for _, arg := range action.Arguments {
if strings.HasPrefix(arg.Name, ReservedArgumentNamePrefix) {
return fmt.Errorf("action %q argument %q uses reserved prefix %q", action.Title, arg.Name, ReservedArgumentNamePrefix)
}
}
return nil
}
func (cfg *Config) sanitizeDashboardsForInlineActions() {
for _, dashboard := range cfg.Dashboards {
cfg.sanitizeDashboardComponentForInlineActions(dashboard)
}
}
func (cfg *Config) sanitizeDashboardComponentForInlineActions(component *DashboardComponent) {
visited := make(map[*DashboardComponent]bool)
cfg.sanitizeDashboardComponentForInlineActionsHelper(component, visited)
}
func (cfg *Config) sanitizeDashboardComponentForInlineActionsHelper(component *DashboardComponent, visited map[*DashboardComponent]bool) {
if component == nil {
return
}
if visited[component] {
return
}
visited[component] = true
cfg.sanitizeInlineAction(component)
cfg.sanitizeChildDashboardComponents(component, visited)
}
func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
if component.InlineAction == nil {
return
}
sanitizeInlineActionTitles(component)
if component.Entity != "" && component.InlineAction.Entity == "" {
component.InlineAction.Entity = component.Entity
}
component.InlineAction.sanitize(cfg)
cfg.addInlineActionIfNotExists(component.InlineAction)
}
func (cfg *Config) addInlineActionIfNotExists(action *Action) {
if cfg.inlineActionExists(action) {
return
}
cfg.Actions = append(cfg.Actions, action)
}
func sanitizeInlineActionTitles(component *DashboardComponent) {
if component.InlineAction.Title == "" {
component.InlineAction.Title = component.Title
}
if component.Title == "" {
component.Title = component.InlineAction.Title
}
}
func (cfg *Config) inlineActionExists(action *Action) bool {
if cfg.inlineActionPointerExists(action) {
return true
}
if cfg.inlineActionIDExists(action) {
return true
}
return false
}
func (cfg *Config) inlineActionPointerExists(action *Action) bool {
for _, existingAction := range cfg.Actions {
if existingAction == action {
return true
}
}
return false
}
func (cfg *Config) inlineActionIDExists(action *Action) bool {
if action.ID == "" {
return false
}
for _, existingAction := range cfg.Actions {
if existingAction.ID == action.ID {
return true
}
}
return false
}
func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent, visited map[*DashboardComponent]bool) {
for _, child := range component.Contents {
if child.Entity == "" {
child.Entity = component.Entity
}
cfg.sanitizeDashboardComponentForInlineActionsHelper(child, visited)
}
}
func (cfg *Config) sanitizeLogLevel() {
if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil {
log.Info("Setting log level to ", logLevel)
log.SetLevel(logLevel)
}
}
func (action *Action) sanitize(cfg *Config) {
if action.Timeout < 3 {
action.Timeout = 3
}
action.ID = getActionID(action)
action.Icon = lookupHTMLIcon(action.Icon, cfg.DefaultIconForActions)
action.PopupOnStart = sanitizePopupOnStart(action.PopupOnStart, cfg)
if action.MaxConcurrent < 1 {
action.MaxConcurrent = 1
}
for idx := range action.Arguments {
action.Arguments[idx].sanitize()
}
}
func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
if cfg.AuthRequireGuestsToLogin {
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
cfg.DefaultPermissions.View = false
cfg.DefaultPermissions.Exec = false
cfg.DefaultPermissions.Logs = false
cfg.DefaultPermissions.Kill = false
}
}
func (cfg *Config) sanitizeLogHistoryPageSize() {
if cfg.LogHistoryPageSize < 10 {
log.Warnf("LogsHistoryLimit is too low, setting it to 10")
cfg.LogHistoryPageSize = 10
} else if cfg.LogHistoryPageSize > 100 {
log.Warnf("LogsHistoryLimit is high, you can do this, but expect browser lag.")
}
}
func (cfg *Config) sanitizeLocalUsers() {
for _, user := range cfg.AuthLocalUsers.Users {
expandLocalUserEnvTemplates(user)
}
if err := validateUniqueLocalUserAPIKeys(cfg.AuthLocalUsers.Users); err != nil {
log.Fatalf("%v", err)
}
}
func expandLocalUserEnvTemplates(user *LocalUser) {
if user == nil {
return
}
if user.Password != "" {
user.Password = expandEnvTemplate(user.Password)
}
if user.ApiKey != "" {
user.ApiKey = expandEnvTemplate(user.ApiKey)
}
}
// validateUniqueLocalUserAPIKeys returns an error when two local users share the same non-empty apiKey.
func validateUniqueLocalUserAPIKeys(users []*LocalUser) error {
seen := make(map[string]string)
for _, user := range users {
if err := recordUniqueLocalUserAPIKey(seen, user); err != nil {
return err
}
}
return nil
}
func recordUniqueLocalUserAPIKey(seen map[string]string, user *LocalUser) error {
if user == nil || user.ApiKey == "" {
return nil
}
if prior, ok := seen[user.ApiKey]; ok {
return fmt.Errorf("duplicate authLocalUsers apiKey for users %q and %q", prior, user.Username)
}
seen[user.ApiKey] = user.Username
return nil
}
func (cfg *Config) sanitizeSecurityHeaders() {
cfg.sanitizeSecurityHeadersCSP()
cfg.sanitizeSecurityHeadersXFrameOptions()
}
func (cfg *Config) sanitizeSecurityHeadersCSP() {
if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" {
return
}
cfg.Security.ContentSecurityPolicy = ContentSecurityPolicyDefault
}
func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() {
if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions != "" {
return
}
cfg.Security.XFrameOptions = "DENY"
}
// expandEnvTemplate expands {{ .Env.VAR }} in config strings using the process environment.
func expandEnvTemplate(source string) string {
t, err := template.New("envTemplate").Option("missingkey=error").Parse(source)
if err != nil {
log.WithFields(log.Fields{"error": err}).Debug("Env template parse failed, using literal")
return source
}
var b strings.Builder
if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil {
log.WithFields(log.Fields{"error": err}).Debug("Env template execute failed, using literal")
return source
}
return b.String()
}
func getActionID(action *Action) string {
if action.ID == "" {
return uuid.NewString()
}
if strings.Contains(action.ID, "{{") {
log.Fatalf("Action IDs cannot contain variables")
}
return action.ID
}
//gocyclo:ignore
func sanitizePopupOnStart(raw string, cfg *Config) string {
switch raw {
case "execution-dialog":
return raw
case "execution-dialog-output-html":
return raw
case "execution-dialog-stdout-only":
return raw
case "execution-button":
return raw
case "history":
return raw
default:
return cfg.DefaultPopupOnStart
}
}
func (arg *ActionArgument) sanitize() {
if arg.Title == "" {
arg.Title = arg.Name
}
for idx, choice := range arg.Choices {
if choice.Title == "" {
arg.Choices[idx].Title = choice.Value
}
}
arg.sanitizeNoType()
// Default value validation runs in executor at config load (validateArgumentDefaults).
}
func (arg *ActionArgument) sanitizeNoType() {
if len(arg.Choices) == 0 && arg.Type == "" {
log.WithFields(log.Fields{
"arg": arg.Name,
}).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.")
arg.Type = "ascii"
}
}