Skip to content

Commit 07b49d1

Browse files
Surya Ahujafacebook-github-bot
authored andcommitted
resolve additional matching issue (#11)
Summary: Pull Request resolved: #11 Go's Match string function is similar to a substring matcher, as opposed to python's regex `match` function which matches from the beginning of the string. For example `cat.*` will match the command `bash /etc/some_folder/file_name_contains_cat.sh` as per go matchers. An easy way to restrict this is to add boundary characters inside expressions as tacquito reads them. There are two places we could add these boundary characters 1) config 2) the server code Within facebook's infra , it would be okay to attach this to either of the places since we control the config. But the stringy module is a part of the OSS modules, and in that case it's better to add the guardrails to the server code so the boundaries are not *optional* Reviewed By: elizabethjoshi Differential Revision: D64148191 fbshipit-source-id: 16f45c8a3e21c4d5d0509d9fe20a52fdedcbc22c
1 parent 1a8d7e6 commit 07b49d1

4 files changed

Lines changed: 122 additions & 3 deletions

File tree

authorize_fields.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (t Args) Validate(condition interface{}) error {
134134
return nil
135135
}
136136

137-
// String returns Args as string, ignoring <cr> cmd-arg=<cr>
137+
// String returns Args as string
138138
func (t Args) String() string {
139139
var b strings.Builder
140140
for _, arg := range t {
@@ -194,6 +194,28 @@ func (t Args) CommandArgs() string {
194194
return strings.Join(args, " ")
195195
}
196196

197+
// isLineEnding returns true if a is a valid line ending for tacacs authorization
198+
// payload
199+
func isLineEnding(a string) bool {
200+
return a == "<cr>" || a == "<CR>"
201+
}
202+
203+
// CommandArgsNoLE joins all cmd-arg args into a single string
204+
// and ignores line endings, specifically <cr>
205+
func (t Args) CommandArgsNoLE() string {
206+
args := make([]string, 0, len(t))
207+
for idx, arg := range t {
208+
a, _, v := arg.ASV()
209+
if a == "cmd-arg" {
210+
if idx == len(t)-1 && isLineEnding(v) {
211+
continue
212+
}
213+
args = append(args, v)
214+
}
215+
}
216+
return strings.Join(args, " ")
217+
}
218+
197219
// Args splits the Args into cmd, cmd-arg and other=arg
198220
// the key is the left side of the delimiter, etc
199221
func (t Args) Args() []string {

authorize_fields_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright (c) Facebook, Inc. and its affiliates.
3+
4+
This source code is licensed under the MIT license found in the
5+
LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package tacquito
9+
10+
import (
11+
"testing"
12+
)
13+
14+
func TestArgsStripCR(t *testing.T) {
15+
args := Args{
16+
"cmd=show",
17+
"cmd-arg=version",
18+
"cmd-arg=<cr>",
19+
}
20+
expected := "version"
21+
if v := args.CommandArgsNoLE(); v != expected {
22+
t.Fatalf("failed to get command args, expected %s, got %s", expected, v)
23+
}
24+
}
25+
26+
func TestArgsStripCRInMiddle(t *testing.T) {
27+
args := Args{
28+
"cmd=show",
29+
"cmd-arg=version",
30+
"cmd-arg=<cr>",
31+
"cmd-arg=actual",
32+
"cmd-arg=line",
33+
"cmd-arg=ending",
34+
"cmd-arg=<cr>",
35+
}
36+
expected := "version <cr> actual line ending"
37+
if v := args.CommandArgsNoLE(); v != expected {
38+
t.Fatalf("failed to get command args, expected %s, got %s", expected, v)
39+
}
40+
}

cmds/server/config/authorizers/stringy/command.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ import (
1616
"github.com/facebookincubator/tacquito/cmds/server/config"
1717
)
1818

19+
const (
20+
// default anchors for regex expressions embedded in command match attributes
21+
// stored as bytes and strs for matching and concatenation
22+
regexStartByte = '^'
23+
regexEndByte = '$'
24+
regexStartStr = "^"
25+
regexEndStr = "$"
26+
)
27+
1928
// NewCommandBasedAuthorizer will return a CommandBasedAuthorizer authorizer. If initial request params
2029
// are not suitable for command based, it returns nil
2130
func NewCommandBasedAuthorizer(ctx context.Context, l loggerProvider, b tq.AuthorRequest, u config.User) *CommandBasedAuthorizer {
@@ -80,7 +89,6 @@ func (a CommandBasedAuthorizer) evaluate() bool {
8089
return false
8190
}
8291
}
83-
8492
for _, c := range a.user.Commands {
8593
c.TrimSpace()
8694
if c.Name == "*" {
@@ -94,8 +102,19 @@ func (a CommandBasedAuthorizer) evaluate() bool {
94102
// cmd matches, but we have no conditions, so match it
95103
return returnBool(c.Action)
96104
}
105+
97106
for _, regexish := range c.Match {
98-
if matched, err := regexp.MatchString(regexish, a.body.Args.CommandArgs()); err != nil {
107+
if len(regexish) == 0 {
108+
continue
109+
}
110+
// guard against regexes that are not anchored to the start and end of the string
111+
if regexish[0] != regexStartByte {
112+
regexish = regexStartStr + regexish
113+
}
114+
if regexish[len(regexish)-1] != regexEndByte {
115+
regexish = regexish + regexEndStr
116+
}
117+
if matched, err := regexp.MatchString(regexish, a.body.Args.CommandArgsNoLE()); err != nil {
99118
a.Errorf(a.ctx, "bad regex detected; %v", err)
100119
return false
101120
} else if matched {

cmds/server/config/authorizers/stringy/test/stringy_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,44 @@ func TestCommands(t *testing.T) {
152152
}
153153
},
154154
},
155+
{
156+
name: "cisco; service=shell, cmd=check for regex boundaries",
157+
user: config.User{
158+
Name: "cisco",
159+
Commands: []config.Command{
160+
{
161+
Name: "bash",
162+
Match: []string{"cat.*"},
163+
Action: config.PERMIT,
164+
},
165+
},
166+
},
167+
request: newAuthorRequest("cisco", tq.Args{"service=shell", "cmd=bash", "cmd-arg=/etc/some_folder/file_name_contains_cat.sh"}),
168+
validate: func(name string, response *mockedResponse) {
169+
if response.got.Status != tq.AuthorStatusFail {
170+
assert.Fail(t, fmt.Sprintf("[%v] should have had a status of [%v] but got [%v]", name, tq.AuthorStatusFail, response.got.Status))
171+
}
172+
},
173+
},
174+
{
175+
name: "cisco; service=shell, cmd=check that boundaries are not added if they are already set",
176+
user: config.User{
177+
Name: "cisco",
178+
Commands: []config.Command{
179+
{
180+
Name: "bash",
181+
Match: []string{"^cat.*$"},
182+
Action: config.PERMIT,
183+
},
184+
},
185+
},
186+
request: newAuthorRequest("cisco", tq.Args{"service=shell", "cmd=bash", "cmd-arg=cat", "cmd-arg=/etc/some_folder/file_name_contains_cat.sh"}),
187+
validate: func(name string, response *mockedResponse) {
188+
if response.got.Status != tq.AuthorStatusPassAdd {
189+
assert.Fail(t, fmt.Sprintf("[%v] should have had a status of [%v] but got [%v]", name, tq.AuthorStatusPassAdd, response.got.Status))
190+
}
191+
},
192+
},
155193
}
156194
for _, test := range tests {
157195
logger.Infof(ctx, "running test [%v]", test.name)

0 commit comments

Comments
 (0)