Home | History | Annotate | Download | only in app
      1 // Copyright 2017 syzkaller project authors. All rights reserved.
      2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
      3 
      4 package dash
      5 
      6 import (
      7 	"encoding/json"
      8 	"fmt"
      9 	"regexp"
     10 	"time"
     11 
     12 	"github.com/google/syzkaller/pkg/email"
     13 )
     14 
     15 // There are multiple configurable aspects of the app (namespaces, reporting, API clients, etc).
     16 // The exact config is stored in a global config variable and is read-only.
     17 // Also see config_stub.go.
     18 type GlobalConfig struct {
     19 	// Min access levels specified hierarchically throughout the config.
     20 	AccessLevel AccessLevel
     21 	// Email suffix of authorized users (e.g. "@foobar.com").
     22 	AuthDomain string
     23 	// Google Analytics Tracking ID.
     24 	AnalyticsTrackingID string
     25 	// Global API clients that work across namespaces (e.g. external reporting).
     26 	Clients map[string]string
     27 	// List of emails blacklisted from issuing test requests.
     28 	EmailBlacklist []string
     29 	// Per-namespace config.
     30 	// Namespaces are a mechanism to separate groups of different kernels.
     31 	// E.g. Debian 4.4 kernels and Ubuntu 4.9 kernels.
     32 	// Each namespace has own reporting config, own API clients
     33 	// and bugs are not merged across namespaces.
     34 	Namespaces map[string]*Config
     35 	// Maps full repository address/branch to description of this repo.
     36 	KernelRepos map[string]KernelRepo
     37 }
     38 
     39 // Per-namespace config.
     40 type Config struct {
     41 	// See GlobalConfig.AccessLevel.
     42 	AccessLevel AccessLevel
     43 	// Name used in UI.
     44 	DisplayTitle string
     45 	// URL of a source coverage report for this namespace
     46 	// (uploading/updating the report is out of scope of the system for now).
     47 	CoverLink string
     48 	// Per-namespace clients that act only on a particular namespace.
     49 	Clients map[string]string
     50 	// A unique key for hashing, can be anything.
     51 	Key string
     52 	// Mail bugs without reports (e.g. "no output").
     53 	MailWithoutReport bool
     54 	// How long should we wait before reporting a bug.
     55 	ReportingDelay time.Duration
     56 	// How long should we wait for a C repro before reporting a bug.
     57 	WaitForRepro time.Duration
     58 	// Managers contains some special additional info about syz-manager instances.
     59 	Managers map[string]ConfigManager
     60 	// Reporting config.
     61 	Reporting []Reporting
     62 }
     63 
     64 // ConfigManager describes a single syz-manager instance.
     65 // Dashboard does not generally need to know about all of them,
     66 // but in some special cases it needs to know some additional information.
     67 type ConfigManager struct {
     68 	Decommissioned bool   // The instance is no longer active.
     69 	DelegatedTo    string // If Decommissioned, test requests should go to this instance instead.
     70 	// Normally instances can test patches on any tree.
     71 	// However, some (e.g. non-upstreamed KMSAN) can test only on a fixed tree.
     72 	// RestrictedTestingRepo contains the repo for such instances
     73 	// and RestrictedTestingReason contains a human readable reason for the restriction.
     74 	RestrictedTestingRepo   string
     75 	RestrictedTestingReason string
     76 }
     77 
     78 // One reporting stage.
     79 type Reporting struct {
     80 	// See GlobalConfig.AccessLevel.
     81 	AccessLevel AccessLevel
     82 	// A unique name (the app does not care about exact contents).
     83 	Name string
     84 	// Name used in UI.
     85 	DisplayTitle string
     86 	// Filter can be used to conditionally skip this reporting or hold off reporting.
     87 	Filter ReportingFilter
     88 	// How many new bugs report per day.
     89 	DailyLimit int
     90 	// Type of reporting and its configuration.
     91 	// The app has one built-in type, EmailConfig, which reports bugs by email.
     92 	// And ExternalConfig which can be used to attach any external reporting system (e.g. Bugzilla).
     93 	Config ReportingType
     94 }
     95 
     96 type ReportingType interface {
     97 	// Type returns a unique string that identifies this reporting type (e.g. "email").
     98 	Type() string
     99 	// NeedMaintainers says if this reporting requires non-empty maintainers list.
    100 	NeedMaintainers() bool
    101 	// Validate validates the current object, this is called only during init.
    102 	Validate() error
    103 }
    104 
    105 type KernelRepo struct {
    106 	// Alias is a short, readable name of a kernel repository.
    107 	Alias string
    108 	// ReportingPriority says if we need to prefer to report crashes in this
    109 	// repo over crashes in repos with lower value. Must be in [0-9] range.
    110 	ReportingPriority int
    111 }
    112 
    113 var (
    114 	clientNameRe = regexp.MustCompile("^[a-zA-Z0-9-_]{4,100}$")
    115 	clientKeyRe  = regexp.MustCompile("^[a-zA-Z0-9]{16,128}$")
    116 )
    117 
    118 type (
    119 	FilterResult    int
    120 	ReportingFilter func(bug *Bug) FilterResult
    121 )
    122 
    123 const (
    124 	FilterReport FilterResult = iota // Report bug in this reporting (default).
    125 	FilterSkip                       // Skip this reporting and proceed to the next one.
    126 	FilterHold                       // Hold off with reporting this bug.
    127 )
    128 
    129 func (cfg *Config) ReportingByName(name string) *Reporting {
    130 	for i := range cfg.Reporting {
    131 		reporting := &cfg.Reporting[i]
    132 		if reporting.Name == name {
    133 			return reporting
    134 		}
    135 	}
    136 	return nil
    137 }
    138 
    139 // config is populated by installConfig which should be called either from tests
    140 // or from a separate file that provides actual production config.
    141 var config *GlobalConfig
    142 
    143 func init() {
    144 	// Prevents gometalinter from considering everything as dead code.
    145 	if false {
    146 		installConfig(nil)
    147 	}
    148 }
    149 
    150 func installConfig(cfg *GlobalConfig) {
    151 	if config != nil {
    152 		panic("another config is already installed")
    153 	}
    154 	// Validate the global cfg.
    155 	if len(cfg.Namespaces) == 0 {
    156 		panic("no namespaces found")
    157 	}
    158 	for i := range cfg.EmailBlacklist {
    159 		cfg.EmailBlacklist[i] = email.CanonicalEmail(cfg.EmailBlacklist[i])
    160 	}
    161 	namespaces := make(map[string]bool)
    162 	clientNames := make(map[string]bool)
    163 	checkClients(clientNames, cfg.Clients)
    164 	checkConfigAccessLevel(&cfg.AccessLevel, AccessPublic, "global")
    165 	for ns, cfg := range cfg.Namespaces {
    166 		checkNamespace(ns, cfg, namespaces, clientNames)
    167 	}
    168 	for repo, info := range cfg.KernelRepos {
    169 		if info.Alias == "" {
    170 			panic(fmt.Sprintf("empty kernel repo alias for %q", repo))
    171 		}
    172 		if prio := info.ReportingPriority; prio < 0 || prio > 9 {
    173 			panic(fmt.Sprintf("bad kernel repo reporting priority %v for %q", prio, repo))
    174 		}
    175 	}
    176 	config = cfg
    177 	initEmailReporting()
    178 	initHTTPHandlers()
    179 	initAPIHandlers()
    180 }
    181 
    182 func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]bool) {
    183 	if ns == "" {
    184 		panic("empty namespace name")
    185 	}
    186 	if namespaces[ns] {
    187 		panic(fmt.Sprintf("duplicate namespace %q", ns))
    188 	}
    189 	namespaces[ns] = true
    190 	if cfg.DisplayTitle == "" {
    191 		cfg.DisplayTitle = ns
    192 	}
    193 	checkClients(clientNames, cfg.Clients)
    194 	for name, mgr := range cfg.Managers {
    195 		checkManager(ns, name, mgr)
    196 	}
    197 	if !clientKeyRe.MatchString(cfg.Key) {
    198 		panic(fmt.Sprintf("bad namespace %q key: %q", ns, cfg.Key))
    199 	}
    200 	if len(cfg.Reporting) == 0 {
    201 		panic(fmt.Sprintf("no reporting in namespace %q", ns))
    202 	}
    203 	checkConfigAccessLevel(&cfg.AccessLevel, cfg.AccessLevel, fmt.Sprintf("namespace %q", ns))
    204 	parentAccessLevel := cfg.AccessLevel
    205 	reportingNames := make(map[string]bool)
    206 	// Go backwards because access levels get stricter backwards.
    207 	for ri := len(cfg.Reporting) - 1; ri >= 0; ri-- {
    208 		reporting := &cfg.Reporting[ri]
    209 		if reporting.Name == "" {
    210 			panic(fmt.Sprintf("empty reporting name in namespace %q", ns))
    211 		}
    212 		if reportingNames[reporting.Name] {
    213 			panic(fmt.Sprintf("duplicate reporting name %q", reporting.Name))
    214 		}
    215 		if reporting.DisplayTitle == "" {
    216 			reporting.DisplayTitle = reporting.Name
    217 		}
    218 		checkConfigAccessLevel(&reporting.AccessLevel, parentAccessLevel,
    219 			fmt.Sprintf("reporting %q/%q", ns, reporting.Name))
    220 		parentAccessLevel = reporting.AccessLevel
    221 		if reporting.Filter == nil {
    222 			reporting.Filter = func(bug *Bug) FilterResult { return FilterReport }
    223 		}
    224 		reportingNames[reporting.Name] = true
    225 		if reporting.Config.Type() == "" {
    226 			panic(fmt.Sprintf("empty reporting type for %q", reporting.Name))
    227 		}
    228 		if err := reporting.Config.Validate(); err != nil {
    229 			panic(err)
    230 		}
    231 		if _, err := json.Marshal(reporting.Config); err != nil {
    232 			panic(fmt.Sprintf("failed to json marshal %q config: %v",
    233 				reporting.Name, err))
    234 		}
    235 	}
    236 }
    237 
    238 func checkManager(ns, name string, mgr ConfigManager) {
    239 	if mgr.Decommissioned && mgr.DelegatedTo == "" {
    240 		panic(fmt.Sprintf("decommissioned manager %v/%v does not have delegate", ns, name))
    241 	}
    242 	if !mgr.Decommissioned && mgr.DelegatedTo != "" {
    243 		panic(fmt.Sprintf("non-decommissioned manager %v/%v has delegate", ns, name))
    244 	}
    245 	if mgr.RestrictedTestingRepo != "" && mgr.RestrictedTestingReason == "" {
    246 		panic(fmt.Sprintf("restricted manager %v/%v does not have restriction reason", ns, name))
    247 	}
    248 	if mgr.RestrictedTestingRepo == "" && mgr.RestrictedTestingReason != "" {
    249 		panic(fmt.Sprintf("unrestricted manager %v/%v has restriction reason", ns, name))
    250 	}
    251 }
    252 
    253 func checkConfigAccessLevel(current *AccessLevel, parent AccessLevel, what string) {
    254 	verifyAccessLevel(parent)
    255 	if *current == 0 {
    256 		*current = parent
    257 	}
    258 	verifyAccessLevel(*current)
    259 	if *current < parent {
    260 		panic(fmt.Sprintf("bad %v access level %v", what, *current))
    261 	}
    262 }
    263 
    264 func checkClients(clientNames map[string]bool, clients map[string]string) {
    265 	for name, key := range clients {
    266 		if !clientNameRe.MatchString(name) {
    267 			panic(fmt.Sprintf("bad client name: %v", name))
    268 		}
    269 		if !clientKeyRe.MatchString(key) {
    270 			panic(fmt.Sprintf("bad client key: %v", key))
    271 		}
    272 		if clientNames[name] {
    273 			panic(fmt.Sprintf("duplicate client name: %v", name))
    274 		}
    275 		clientNames[name] = true
    276 	}
    277 }
    278