Repository navigation
feat(tools): opt-in read-only mode with readOnlyHint annotations #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
strelok89
wants to merge
4
commits into
firebolt-db:main
Choose a base branch
from
bigabid:DO-259-read-only-mode
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
3040e8c
feat(tools): opt-in read-only mode with readOnlyHint annotations (DO-…
strelok89 6886a78
fix(tools): treat dollar-quoted strings as literals in read-only check
strelok89 a94c60b
fix(tools): scan dollar quotes that follow a keyword in read-only check
strelok89 b354250
Merge branch 'main' into DO-259-read-only-mode
strelok89 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package tool_query | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
| "unicode" | ||
| ) | ||
|
|
||
| // writeKeywords are rejected anywhere in a read-only query, outside of literals and comments. | ||
| // This catches data-modifying CTEs such as `WITH x AS (...) INSERT INTO ...`. | ||
| var writeKeywords = map[string]struct{}{ | ||
| "INSERT": {}, "UPDATE": {}, "DELETE": {}, "MERGE": {}, "TRUNCATE": {}, "COPY": {}, | ||
| "CREATE": {}, "ALTER": {}, "DROP": {}, "UNDROP": {}, "VACUUM": {}, | ||
| "GRANT": {}, "REVOKE": {}, "CALL": {}, | ||
| } | ||
|
|
||
| var errMultipleStatements = errors.New("bad request: read-only mode: only a single statement is allowed") | ||
|
|
||
| // validateReadOnly accepts a single SELECT or WITH statement and rejects anything else. | ||
| // It is defence in depth on top of the Firebolt role's grants, not a substitute for them: | ||
| // a keyword scan can be bypassed, the role cannot. | ||
| func validateReadOnly(sql string) error { | ||
| words, err := scanWords(sql) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if len(words) == 0 { | ||
| return errors.New("bad request: read-only mode: query contains no statement") | ||
| } | ||
|
|
||
| if first := words[0]; first != "SELECT" && first != "WITH" { | ||
| return fmt.Errorf("bad request: read-only mode: only SELECT or WITH statements are allowed, got %s", first) | ||
| } | ||
| for _, w := range words { | ||
| if _, ok := writeKeywords[w]; ok { | ||
| return fmt.Errorf("bad request: read-only mode: %s is not allowed", w) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // scanWords returns the upper-cased bare words of sql, skipping string literals, quoted | ||
| // identifiers and comments. It fails if anything but comments follows a `;`. | ||
| func scanWords(sql string) ([]string, error) { | ||
| var ( | ||
| words []string | ||
| ended bool | ||
| r = []rune(sql) | ||
| n = len(r) | ||
| ) | ||
|
|
||
| for i := 0; i < n; { | ||
| c := r[i] | ||
| switch { | ||
| case unicode.IsSpace(c): | ||
| i++ | ||
| case c == '-' && i+1 < n && r[i+1] == '-': | ||
| for i < n && r[i] != '\n' { | ||
| i++ | ||
| } | ||
| case c == '/' && i+1 < n && r[i+1] == '*': | ||
| j := i + 2 | ||
| for j+1 < n && (r[j] != '*' || r[j+1] != '/') { | ||
| j++ | ||
| } | ||
| if j+1 >= n { | ||
| return nil, errors.New("bad request: read-only mode: unterminated comment") | ||
| } | ||
| i = j + 2 | ||
| case ended: | ||
| return nil, errMultipleStatements | ||
| case c == ';': | ||
| ended = true | ||
| i++ | ||
| case c == '$' && (i == 0 || !isIdentPart(r[i-1])): | ||
| j, err := skipDollarQuoted(r, i) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| i = j | ||
| case c == '\'' || c == '"': | ||
| backslashEscapes := c == '\'' && isEscapeStringPrefix(r, i) | ||
| j, err := skipQuoted(r, i, backslashEscapes) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| i = j | ||
| case isIdentStart(c): | ||
| j := i | ||
| for j < n && isIdentPart(r[j]) { | ||
| j++ | ||
| } | ||
| words = append(words, strings.ToUpper(string(r[i:j]))) | ||
| i = j | ||
| default: | ||
| i++ | ||
| } | ||
| } | ||
|
|
||
| return words, nil | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| // skipQuoted returns the index just past the quoted run starting at r[start]. | ||
| // A doubled quote is an escaped quote; in E'...' strings a backslash also escapes. | ||
| func skipQuoted(r []rune, start int, backslashEscapes bool) (int, error) { | ||
| quote := r[start] | ||
| for i := start + 1; i < len(r); i++ { | ||
| switch { | ||
| case backslashEscapes && r[i] == '\\': | ||
| i++ | ||
| case r[i] != quote: | ||
| case i+1 < len(r) && r[i+1] == quote: | ||
| i++ | ||
| default: | ||
| return i + 1, nil | ||
| } | ||
| } | ||
| return 0, errors.New("bad request: read-only mode: unterminated quoted string") | ||
| } | ||
|
|
||
| // skipDollarQuoted returns the index just past a $$...$$ or $tag$...$tag$ literal starting | ||
| // at r[start], or start+1 when the `$` does not open one (for example a $1 parameter). | ||
| func skipDollarQuoted(r []rune, start int) (int, error) { | ||
| j := start + 1 | ||
| if j < len(r) && isIdentStart(r[j]) { | ||
| for j < len(r) && (isIdentStart(r[j]) || unicode.IsDigit(r[j])) { | ||
| j++ | ||
| } | ||
| } | ||
| if j >= len(r) || r[j] != '$' { | ||
| return start + 1, nil | ||
| } | ||
| tag := r[start : j+1] | ||
| for k := j + 1; k+len(tag) <= len(r); k++ { | ||
| if string(r[k:k+len(tag)]) == string(tag) { | ||
| return k + len(tag), nil | ||
| } | ||
| } | ||
| return 0, errors.New("bad request: read-only mode: unterminated dollar-quoted string") | ||
| } | ||
|
|
||
| // isEscapeStringPrefix reports whether the quote at r[i] opens an E'...' escape string. | ||
| func isEscapeStringPrefix(r []rune, i int) bool { | ||
| if i == 0 || (r[i-1] != 'E' && r[i-1] != 'e') { | ||
| return false | ||
| } | ||
| return i == 1 || !isIdentPart(r[i-2]) | ||
| } | ||
|
|
||
| func isIdentStart(c rune) bool { | ||
| return unicode.IsLetter(c) || c == '_' | ||
| } | ||
|
|
||
| func isIdentPart(c rune) bool { | ||
| return unicode.IsLetter(c) || unicode.IsDigit(c) || c == '_' || c == '$' | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.