mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
chore: Rewrite rbac rego -> SQL clause (#5138)
* chore: Rewrite rbac rego -> SQL clause Previous code was challenging to read with edge cases - bug: OrgAdmin could not make new groups - Also refactor some function names
This commit is contained in:
307
coderd/rbac/regosql/compile_test.go
Normal file
307
coderd/rbac/regosql/compile_test.go
Normal file
@ -0,0 +1,307 @@
|
||||
package regosql_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac/regosql"
|
||||
"github.com/coder/coder/coderd/rbac/regosql/sqltypes"
|
||||
)
|
||||
|
||||
// TestRegoQueriesNoVariables handles cases without variables. These should be
|
||||
// very simple and straight forward.
|
||||
func TestRegoQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := func(v string) string {
|
||||
return "(" + v + ")"
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Queries []string
|
||||
ExpectedSQL string
|
||||
ExpectError bool
|
||||
ExpectedSQLGenError bool
|
||||
|
||||
VariableConverter sqltypes.VariableMatcher
|
||||
}{
|
||||
{
|
||||
Name: "Empty",
|
||||
Queries: []string{``},
|
||||
ExpectedSQL: "true",
|
||||
},
|
||||
{
|
||||
Name: "True",
|
||||
Queries: []string{`true`},
|
||||
ExpectedSQL: "true",
|
||||
},
|
||||
{
|
||||
Name: "False",
|
||||
Queries: []string{`false`},
|
||||
ExpectedSQL: "false",
|
||||
},
|
||||
{
|
||||
Name: "MultipleBool",
|
||||
Queries: []string{"true", "false"},
|
||||
ExpectedSQL: "(true OR false)",
|
||||
},
|
||||
{
|
||||
Name: "Numbers",
|
||||
Queries: []string{
|
||||
"(1 != 2) = true",
|
||||
"5 == 5",
|
||||
},
|
||||
ExpectedSQL: p("((1 != 2) = true) OR (5 = 5)"),
|
||||
},
|
||||
// Variables
|
||||
{
|
||||
// Always return a constant string for all variables.
|
||||
Name: "V_Basic",
|
||||
Queries: []string{
|
||||
`input.x = "hello_world"`,
|
||||
},
|
||||
ExpectedSQL: p("only_var = 'hello_world'"),
|
||||
VariableConverter: sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
sqltypes.StringVarMatcher("only_var", []string{
|
||||
"input", "x",
|
||||
}),
|
||||
),
|
||||
},
|
||||
// Coder Variables
|
||||
{
|
||||
// Always return a constant string for all variables.
|
||||
Name: "GroupACL",
|
||||
Queries: []string{
|
||||
`"read" in input.object.acl_group_list.allUsers`,
|
||||
},
|
||||
ExpectedSQL: "(group_acl->'allUsers' ? 'read')",
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "GroupWildcard",
|
||||
Queries: []string{`"*" in input.object.acl_group_list.allUsers`},
|
||||
ExpectedSQL: "(group_acl->'allUsers' ? '*')",
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
// Always return a constant string for all variables.
|
||||
Name: "GroupACLWithVarField",
|
||||
Queries: []string{
|
||||
`"read" in input.object.acl_group_list[input.object.org_owner]`,
|
||||
},
|
||||
ExpectedSQL: "(group_acl->organization_id :: text ? 'read')",
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "VarInArray",
|
||||
Queries: []string{
|
||||
`input.object.org_owner in {"a", "b", "c"}`,
|
||||
},
|
||||
ExpectedSQL: p("organization_id :: text = ANY(ARRAY ['a','b','c'])"),
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "SetDereference",
|
||||
Queries: []string{`"*" in input.object.acl_group_list[input.object.org_owner]`},
|
||||
ExpectedSQL: p("group_acl->organization_id :: text ? '*'"),
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "JsonbLiteralDereference",
|
||||
Queries: []string{`"*" in input.object.acl_group_list["4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75"]`},
|
||||
ExpectedSQL: p("group_acl->'4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75' ? '*'"),
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "Complex",
|
||||
Queries: []string{
|
||||
`input.object.org_owner != ""`,
|
||||
`input.object.org_owner in {"a", "b", "c"}`,
|
||||
`input.object.org_owner != ""`,
|
||||
`"read" in input.object.acl_group_list.allUsers`,
|
||||
`"read" in input.object.acl_user_list.me`,
|
||||
},
|
||||
ExpectedSQL: `((organization_id :: text != '') OR ` +
|
||||
`(organization_id :: text = ANY(ARRAY ['a','b','c'])) OR ` +
|
||||
`(organization_id :: text != '') OR ` +
|
||||
`(group_acl->'allUsers' ? 'read') OR ` +
|
||||
`(user_acl->'me' ? 'read'))`,
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "NoACLs",
|
||||
Queries: []string{
|
||||
`"read" in input.object.acl_group_list[input.object.org_owner]`,
|
||||
`"*" in input.object.acl_group_list["4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75"]`,
|
||||
},
|
||||
// Special case where the bool is wrapped
|
||||
ExpectedSQL: p("(false) OR (false)"),
|
||||
VariableConverter: regosql.NoACLConverter(),
|
||||
},
|
||||
{
|
||||
Name: "TwoExpressions",
|
||||
Queries: []string{
|
||||
`true; true`,
|
||||
},
|
||||
ExpectedSQL: p("true AND true"),
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
|
||||
// Actual vectors from production
|
||||
{
|
||||
Name: "FromOwner",
|
||||
Queries: []string{
|
||||
``,
|
||||
`"05f58202-4bfc-43ce-9ba4-5ff6e0174a71" = input.object.org_owner`,
|
||||
`"read" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`,
|
||||
},
|
||||
ExpectedSQL: "true",
|
||||
VariableConverter: regosql.NoACLConverter(),
|
||||
},
|
||||
{
|
||||
Name: "OrgAdmin",
|
||||
Queries: []string{
|
||||
`input.object.org_owner != "";
|
||||
input.object.org_owner in {"05f58202-4bfc-43ce-9ba4-5ff6e0174a71"};
|
||||
input.object.owner != "";
|
||||
"d5389ccc-57a4-4b13-8c3f-31747bcdc9f1" = input.object.owner`,
|
||||
},
|
||||
ExpectedSQL: "((organization_id :: text != '') AND " +
|
||||
"(organization_id :: text = ANY(ARRAY ['05f58202-4bfc-43ce-9ba4-5ff6e0174a71'])) AND " +
|
||||
"(owner_id :: text != '') AND " +
|
||||
"('d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' = owner_id :: text))",
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "UserACLAllow",
|
||||
Queries: []string{
|
||||
`"read" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`,
|
||||
`"*" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`,
|
||||
},
|
||||
ExpectedSQL: "((user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' ? 'read') OR " +
|
||||
"(user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' ? '*'))",
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "NoACLConfig",
|
||||
Queries: []string{
|
||||
`input.object.org_owner != "";
|
||||
input.object.org_owner in {"05f58202-4bfc-43ce-9ba4-5ff6e0174a71"};
|
||||
"read" in input.object.acl_group_list[input.object.org_owner]`,
|
||||
},
|
||||
ExpectedSQL: "((organization_id :: text != '') AND (organization_id :: text = ANY(ARRAY ['05f58202-4bfc-43ce-9ba4-5ff6e0174a71'])) AND (false))",
|
||||
VariableConverter: regosql.NoACLConverter(),
|
||||
},
|
||||
{
|
||||
Name: "EmptyACLListNoACLs",
|
||||
Queries: []string{
|
||||
`input.object.org_owner != "";
|
||||
input.object.org_owner in set();
|
||||
"create" in input.object.acl_group_list[input.object.org_owner]`,
|
||||
|
||||
`input.object.org_owner != "";
|
||||
input.object.org_owner in set();
|
||||
"*" in input.object.acl_group_list[input.object.org_owner]`,
|
||||
|
||||
`"create" in input.object.acl_user_list.me`,
|
||||
|
||||
`"*" in input.object.acl_user_list.me`,
|
||||
},
|
||||
ExpectedSQL: p(p("(organization_id :: text != '') AND (false) AND (group_acl->organization_id :: text ? 'create')") + " OR " +
|
||||
p("(organization_id :: text != '') AND (false) AND (group_acl->organization_id :: text ? '*')") + " OR " +
|
||||
p("user_acl->'me' ? 'create'") + " OR " +
|
||||
p("user_acl->'me' ? '*'")),
|
||||
VariableConverter: regosql.DefaultVariableConverter(),
|
||||
},
|
||||
{
|
||||
Name: "TemplateOwner",
|
||||
Queries: []string{
|
||||
`neq(input.object.org_owner, "");
|
||||
internal.member_2(input.object.org_owner, {"3bf82434-e40b-44ae-b3d8-d0115bba9bad", "5630fda3-26ab-462c-9014-a88a62d7a415", "c304877a-bc0d-4e9b-9623-a38eae412929"});
|
||||
neq(input.object.owner, "");
|
||||
"806dd721-775f-4c85-9ce3-63fbbd975954" = input.object.owner`,
|
||||
},
|
||||
ExpectedSQL: p(p("organization_id :: text != ''") + " AND " +
|
||||
p("organization_id :: text = ANY(ARRAY ['3bf82434-e40b-44ae-b3d8-d0115bba9bad','5630fda3-26ab-462c-9014-a88a62d7a415','c304877a-bc0d-4e9b-9623-a38eae412929'])") + " AND " +
|
||||
p("false") + " AND " +
|
||||
p("false")),
|
||||
VariableConverter: regosql.TemplateConverter(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
part := partialQueries(tc.Queries...)
|
||||
|
||||
cfg := regosql.ConvertConfig{
|
||||
VariableConverter: tc.VariableConverter,
|
||||
}
|
||||
|
||||
requireConvert(t, convertTestCase{
|
||||
part: part,
|
||||
cfg: cfg,
|
||||
expectSQL: tc.ExpectedSQL,
|
||||
expectConvertError: tc.ExpectError,
|
||||
expectSQLGenError: tc.ExpectedSQLGenError,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type convertTestCase struct {
|
||||
part *rego.PartialQueries
|
||||
cfg regosql.ConvertConfig
|
||||
|
||||
expectConvertError bool
|
||||
expectSQL string
|
||||
expectSQLGenError bool
|
||||
}
|
||||
|
||||
func requireConvert(t *testing.T, tc convertTestCase) {
|
||||
t.Helper()
|
||||
|
||||
for i, q := range tc.part.Queries {
|
||||
t.Logf("Query %d: %s", i, q.String())
|
||||
}
|
||||
for i, s := range tc.part.Support {
|
||||
t.Logf("Support %d: %s", i, s.String())
|
||||
}
|
||||
|
||||
root, err := regosql.ConvertRegoAst(tc.cfg, tc.part)
|
||||
if tc.expectConvertError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err, "compile")
|
||||
|
||||
gen := sqltypes.NewSQLGenerator()
|
||||
sqlString := root.SQLString(gen)
|
||||
if tc.expectSQLGenError {
|
||||
require.True(t, len(gen.Errors()) > 0, "expected SQL generation error")
|
||||
} else {
|
||||
require.NoError(t, err, "sql gen")
|
||||
require.Equal(t, tc.expectSQL, sqlString, "sql match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func partialQueries(queries ...string) *rego.PartialQueries {
|
||||
opts := ast.ParserOptions{
|
||||
AllFutureKeywords: true,
|
||||
}
|
||||
|
||||
astQueries := make([]ast.Body, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
astQueries = append(astQueries, ast.MustParseBodyWithOpts(q, opts))
|
||||
}
|
||||
|
||||
return ®o.PartialQueries{
|
||||
Queries: astQueries,
|
||||
Support: []*ast.Module{},
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user