package regosql_test import ( "testing" "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/v1/rego" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/rbac/regosql" "github.com/coder/coder/v2/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: "AllowList", Queries: []string{ `input.object.id != "" `, `input.object.id in ["9046b041-58ed-47a3-9c3a-de302577875a"]`, }, // Special case where the bool is wrapped ExpectedSQL: p(`(id :: text != '') OR ` + `(id :: text = ANY(ARRAY ['9046b041-58ed-47a3-9c3a-de302577875a']))`), 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(), }, { Name: "UserNoOrgOwner", Queries: []string{ `input.object.org_owner != ""`, }, ExpectedSQL: p("'' != ''"), VariableConverter: regosql.UserConverter(), }, { Name: "UserOwnsSelf", Queries: []string{ `"10d03e62-7703-4df5-a358-4f76577d4e2f" = input.object.owner; input.object.owner != ""; input.object.org_owner = ""`, }, VariableConverter: regosql.UserConverter(), ExpectedSQL: p( p("'10d03e62-7703-4df5-a358-4f76577d4e2f' = id :: text") + " AND " + p("id :: text != ''") + " AND " + p("'' = ''"), ), }, } 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{}, } }