package runs import ( "errors" "strings" "unicode" "unicode/utf8" ) // A `run` service credential carries the command prefixes it may run // (service-runs spec § 1). These are the limits on that list, and the one // function that decides whether an argv is inside it. The console mints // with CheckCommandPrefixes, reads a stored list back through it, and gates // a run's create on CommandGranted. const ( MaxCommandPrefixes = 32 MaxCommandWordBytes = 128 ) var ( ErrNoCommands = errors.New("runs: no command prefixes") ErrTooManyCommands = errors.New("runs: more than 32 command prefixes") ErrEmptyCommand = errors.New("runs: a command prefix has no words") ErrEmptyCommandWord = errors.New("runs: a command prefix holds an empty word") ErrCommandWordTooLong = errors.New("runs: a command word over 128 bytes") ErrCommandWordChars = errors.New("runs: a command word holds a space, a comma or a character that cannot be printed") ) // CheckCommandPrefixes is the shape every stored command list has: at least // one prefix and at most MaxCommandPrefixes, each with at least one word, // each word 1 to MaxCommandWordBytes bytes. A zero-word prefix is refused // because it would match every argv. // // A word is also valid UTF-8 made only of printable characters, with no // whitespace and no comma (validCommandWord). Whitespace and commas are the // text form's separators, so a word holding one would come back as a // different grant when a listing is copied into a new mint; a control // character could hide a prefix in a terminal listing. func CheckCommandPrefixes(prefixes [][]string) error { if len(prefixes) == 0 { return ErrNoCommands } if len(prefixes) > MaxCommandPrefixes { return ErrTooManyCommands } for _, p := range prefixes { if len(p) == 0 { return ErrEmptyCommand } for _, w := range p { if w == "" { return ErrEmptyCommandWord } if len(w) > MaxCommandWordBytes { return ErrCommandWordTooLong } if !validCommandWord(w) { return ErrCommandWordChars } } } return nil } // validCommandWord: valid UTF-8, every rune printable, none a space or a // comma. U+FFFD is refused too: it is what invalid UTF-8 becomes once a // JSON decoder has replaced it, so refusing it makes a mangled word fail // the same way at the API door as at the form. func validCommandWord(w string) bool { if !utf8.ValidString(w) { return false } for _, r := range w { if r == ',' || r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { return false } } return true } // CommandGranted reports whether argv starts with every word of at least one // prefix, compared word for word and exactly: no globbing, no case folding, // no substring. It fails closed: a zero-word prefix matches nothing, and so // does an empty argv. func CommandGranted(argv []string, prefixes [][]string) bool { for _, p := range prefixes { if len(p) == 0 || len(argv) < len(p) { continue } match := true for i, w := range p { if argv[i] != w { match = false break } } if match { return true } } return false } // ParseCommandList reads the text form both mint doors take, the CLI's // --commands flag and the console form's field: prefixes separated by // commas, a prefix's words by spaces. "import classic,import sync" is two // prefixes of two words each. An entry with no words is dropped, the way an // empty rung entry is, so a trailing comma grants nothing rather than // everything. It returns nil when nothing was named; validation is // CheckCommandPrefixes's. func ParseCommandList(s string) [][]string { var out [][]string for _, entry := range strings.Split(s, ",") { if words := strings.Fields(entry); len(words) > 0 { out = append(out, words) } } return out } // UnreadableCommands is what a listing shows, in place of a command list, // for a run credential whose stored list cannot be read. The console and the // CLI both print it. const UnreadableCommands = "(unreadable — refused at sign-in)" // FormatCommandList is a command list for a person to read: // "import classic, import sync". func FormatCommandList(prefixes [][]string) string { parts := make([]string, len(prefixes)) for i, p := range prefixes { parts[i] = strings.Join(p, " ") } return strings.Join(parts, ", ") }