From 8e98ceaa0328e61db636471f295f50912a18e591 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jun 2024 15:39:20 -0500 Subject: [PATCH 1/7] chore: sql parameter to custom roles to be a (name,org) tuple --- coderd/database/dbauthz/customroles_test.go | 16 ++++++++++++- coderd/database/dbmem/dbmem.go | 15 ++++++++----- coderd/database/queries.sql.go | 20 +++++++---------- coderd/database/queries/roles.sql | 14 +++++------- coderd/database/sqlc.yaml | 4 ++++ coderd/database/types.go | 20 +++++++++++++++++ coderd/rbac/rolestore/rolestore.go | 25 ++++++++++++++++++++- 7 files changed, 86 insertions(+), 28 deletions(-) diff --git a/coderd/database/dbauthz/customroles_test.go b/coderd/database/dbauthz/customroles_test.go index a5077121c0629..b98af8fd23889 100644 --- a/coderd/database/dbauthz/customroles_test.go +++ b/coderd/database/dbauthz/customroles_test.go @@ -38,7 +38,7 @@ func TestUpsertCustomRoles(t *testing.T) { Name: "can-assign", DisplayName: "", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceAssignRole.Type: {policy.ActionCreate}, + rbac.ResourceAssignRole.Type: {policy.ActionRead, policy.ActionCreate}, }), } @@ -243,6 +243,20 @@ func TestUpsertCustomRoles(t *testing.T) { require.ErrorContains(t, err, tc.errorContains) } else { require.NoError(t, err) + + // Verify we can fetch the role + roles, err := az.CustomRoles(ctx, database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{ + { + Name: "test-role", + OrganizationID: tc.organizationID.UUID, + }, + }, + ExcludeOrgRoles: false, + OrganizationID: uuid.UUID{}, + }) + require.NoError(t, err) + require.Len(t, roles, 1) } }) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 2bfff39a949a9..7d50cdbaac6e3 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1186,12 +1186,17 @@ func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesPar for _, role := range q.data.customRoles { role := role if len(arg.LookupRoles) > 0 { - if !slices.ContainsFunc(arg.LookupRoles, func(s string) bool { - roleName := rbac.RoleName(role.Name, "") - if role.OrganizationID.UUID != uuid.Nil { - roleName = rbac.RoleName(role.Name, role.OrganizationID.UUID.String()) + if !slices.ContainsFunc(arg.LookupRoles, func(pair database.NameOrganizationPair) bool { + if !strings.EqualFold(pair.Name, role.Name) { + return false } - return strings.EqualFold(s, roleName) + + if role.OrganizationID.Valid { + // Expect org match + return role.OrganizationID.UUID == pair.OrganizationID + } + // Expect no org + return pair.OrganizationID == uuid.Nil }) { continue } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 677f972e734c3..c582ba9d27415 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5604,20 +5604,16 @@ FROM custom_roles WHERE true - -- Lookup roles filter expects the role names to be in the rbac package - -- format. Eg: name[:] - AND CASE WHEN array_length($1 :: text[], 1) > 0 THEN - -- Case insensitive lookup with org_id appended (if non-null). - -- This will return just the name if org_id is null. It'll append - -- the org_id if not null - concat(name, NULLIF(concat(':', organization_id), ':')) ILIKE ANY($1 :: text []) - ELSE true + -- @lookup_roles will filter for exact (role_name, org_id) pairs + AND CASE WHEN array_length($1 :: name_organization_pair_list[], 1) > 0 THEN + (name, organization_id) ILIKE ANY ($1::name_organization_pair_list[]) END - -- Org scoping filter, to only fetch site wide roles + -- This allows fetching all roles, or just site wide roles AND CASE WHEN $2 :: boolean THEN organization_id IS null ELSE true END + -- Allows fetching all roles to a particular organization AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN organization_id = $3 ELSE true @@ -5625,9 +5621,9 @@ WHERE ` type CustomRolesParams struct { - LookupRoles []string `db:"lookup_roles" json:"lookup_roles"` - ExcludeOrgRoles bool `db:"exclude_org_roles" json:"exclude_org_roles"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + LookupRoles []NameOrganizationPair `db:"lookup_roles" json:"lookup_roles"` + ExcludeOrgRoles bool `db:"exclude_org_roles" json:"exclude_org_roles"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` } func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) { diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index dd8816d40eecc..da300763f643c 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -5,20 +5,16 @@ FROM custom_roles WHERE true - -- Lookup roles filter expects the role names to be in the rbac package - -- format. Eg: name[:] - AND CASE WHEN array_length(@lookup_roles :: text[], 1) > 0 THEN - -- Case insensitive lookup with org_id appended (if non-null). - -- This will return just the name if org_id is null. It'll append - -- the org_id if not null - concat(name, NULLIF(concat(':', organization_id), ':')) ILIKE ANY(@lookup_roles :: text []) - ELSE true + -- @lookup_roles will filter for exact (role_name, org_id) pairs + AND CASE WHEN array_length(@lookup_roles :: name_organization_pair_list[], 1) > 0 THEN + (name, organization_id) ILIKE ANY (@lookup_roles::name_organization_pair_list[]) END - -- Org scoping filter, to only fetch site wide roles + -- This allows fetching all roles, or just site wide roles AND CASE WHEN @exclude_org_roles :: boolean THEN organization_id IS null ELSE true END + -- Allows fetching all roles to a particular organization AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN organization_id = @organization_id ELSE true diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index ff8faf5f7704c..3c9c6b24876ed 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -28,6 +28,10 @@ sql: emit_enum_valid_method: true emit_all_enum_values: true overrides: + # Used in 'CustomRoles' query to filter by (name,organization_id) + - db_type: "name_organization_pair_list" + go_type: + type: "NameOrganizationPair" - column: "custom_roles.site_permissions" go_type: type: "CustomRolePermissions" diff --git a/coderd/database/types.go b/coderd/database/types.go index 5d0490d0c9020..bbb4e268011c3 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -142,3 +142,23 @@ func (a CustomRolePermission) String() string { } return str } + +// NameOrganizationPair is used as a lookup tuple for custom role rows. +type NameOrganizationPair struct { + Name string `db:"name" json:"name"` + // OrganizationID if unset will assume a null column value + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` +} + +func (a *NameOrganizationPair) Scan(_ interface{}) error { + return xerrors.Errorf("this should never happen, type 'NameOrganizationPair' should only be used as a parameter") +} + +func (a NameOrganizationPair) Value() (driver.Value, error) { + var orgID interface{} = a.OrganizationID + if a.OrganizationID == uuid.Nil { + orgID = nil + } + + return []interface{}{a.Name, orgID}, nil +} diff --git a/coderd/rbac/rolestore/rolestore.go b/coderd/rbac/rolestore/rolestore.go index 083f03877aa83..80cbd1165073b 100644 --- a/coderd/rbac/rolestore/rolestore.go +++ b/coderd/rbac/rolestore/rolestore.go @@ -69,11 +69,34 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles, } if len(lookup) > 0 { + // The set of roles coming in are formatted as 'rolename[:]'. + // In the database, org roles are scoped with an organization column. + lookupArgs := make([]database.NameOrganizationPair, 0, len(lookup)) + for _, name := range lookup { + roleName, orgID, err := rbac.RoleSplit(name) + if err != nil { + continue + } + + parsedOrgID := uuid.Nil // Default to no org ID + if orgID != "" { + parsedOrgID, err = uuid.Parse(orgID) + if err != nil { + continue + } + } + + lookupArgs = append(lookupArgs, database.NameOrganizationPair{ + Name: roleName, + OrganizationID: parsedOrgID, + }) + } + // If some roles are missing from the database, they are omitted from // the expansion. These roles are no-ops. Should we raise some kind of // warning when this happens? dbroles, err := db.CustomRoles(ctx, database.CustomRolesParams{ - LookupRoles: lookup, + LookupRoles: lookupArgs, ExcludeOrgRoles: false, OrganizationID: uuid.Nil, }) From 4356f53916435c065d74ceac6668f416d318febc Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jun 2024 16:17:53 -0500 Subject: [PATCH 2/7] WIP --- coderd/database/dump.sql | 5 + .../000215_scoped_org_db_roles.up.sql | 2 + coderd/database/querier_test.go | 92 +++++++++++++++++++ coderd/database/queries.sql.go | 4 +- coderd/database/queries/roles.sql | 4 +- coderd/database/sqlc.yaml | 2 +- coderd/database/types.go | 4 +- 7 files changed, 106 insertions(+), 7 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 28adc8a36b1f1..dce976dbef622 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -73,6 +73,11 @@ CREATE TYPE login_type AS ENUM ( COMMENT ON TYPE login_type IS 'Specifies the method of authentication. "none" is a special case in which no authentication method is allowed.'; +CREATE TYPE name_organization_pair AS ( + name text, + organiztion_id uuid +); + CREATE TYPE parameter_destination_scheme AS ENUM ( 'none', 'environment_variable', diff --git a/coderd/database/migrations/000215_scoped_org_db_roles.up.sql b/coderd/database/migrations/000215_scoped_org_db_roles.up.sql index aecd19b8da668..bb959a832a46a 100644 --- a/coderd/database/migrations/000215_scoped_org_db_roles.up.sql +++ b/coderd/database/migrations/000215_scoped_org_db_roles.up.sql @@ -5,3 +5,5 @@ ALTER TABLE ONLY organization_members ALTER COLUMN roles SET DEFAULT '{}'; -- No one should be using organization roles yet. If they are, the names in the -- database are now incorrect. Just remove them all. UPDATE organization_members SET roles = '{}'; + +CREATE TYPE name_organization_pair AS (name text, organiztion_id uuid); diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index c3e1f2e46b3db..4e667261addc6 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -6,6 +6,7 @@ import ( "context" "database/sql" "encoding/json" + "fmt" "sort" "testing" "time" @@ -14,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/migrations" @@ -514,6 +516,96 @@ func TestDefaultOrg(t *testing.T) { require.True(t, all[0].IsDefault, "first org should always be default") } +// TestReadCustomRoles tests the input params returns the correct set of roles. +func TestReadCustomRoles(t *testing.T) { + t.Parallel() + + if testing.Short() { + t.SkipNow() + } + + sqlDB := testSQLDB(t) + err := migrations.Up(sqlDB) + require.NoError(t, err) + db := database.New(sqlDB) + ctx := testutil.Context(t, testutil.WaitLong) + + // Make a few site roles, and a few org roles + orgIDs := make([]uuid.UUID, 3) + for i := range orgIDs { + orgIDs[i] = uuid.New() + } + + roles := make([]database.CustomRole, 0) + for i := 0; i < 15; i++ { + orgID := uuid.NullUUID{} + if i%7 != 0 { + orgID = uuid.NullUUID{ + UUID: orgIDs[i%len(orgIDs)], + Valid: true, + } + } + + role, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{ + Name: fmt.Sprintf("role-%d", i), + OrganizationID: orgID, + }) + require.NoError(t, err) + roles = append(roles, role) + } + + // normalizedRoleName allows for the simple ElementsMatch to work properly. + normalizedRoleName := func(role database.CustomRole) string { + return role.Name + ":" + role.OrganizationID.UUID.String() + } + + testCases := []struct { + Name string + Params database.CustomRoles2Params + Match func(role database.CustomRole) bool + }{ + { + // Empty params should return all roles + Name: "Empty", + Params: database.CustomRoles2Params{}, + Match: func(role database.CustomRole) bool { + return true + }, + }, + //{ + // // Only an organization roles + // Name: "Organization", + // Params: database.CustomRolesParams{}, + // Match: func(role database.CustomRole) bool { + // return true + // }, + //}, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + found, err := db.CustomRoles2(ctx, tc.Params) + require.NoError(t, err) + filtered := make([]database.CustomRole, 0) + for _, role := range roles { + if tc.Match(role) { + filtered = append(filtered, role) + } + } + + a := db2sdk.List(filtered, normalizedRoleName) + var _, _ = found, a + //b := db2sdk.List(found, normalizedRoleName) + //require.Equal(t, a, b) + }) + } +} + type tvArgs struct { Status database.ProvisionerJobStatus // CreateWorkspace is true if we should create a workspace for the template version diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c582ba9d27415..dbec0d4374791 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5605,8 +5605,8 @@ FROM WHERE true -- @lookup_roles will filter for exact (role_name, org_id) pairs - AND CASE WHEN array_length($1 :: name_organization_pair_list[], 1) > 0 THEN - (name, organization_id) ILIKE ANY ($1::name_organization_pair_list[]) + AND CASE WHEN array_length($1 :: name_organization_pair[], 1) > 0 THEN + (name, organization_id) = ANY ($1::name_organization_pair[]) END -- This allows fetching all roles, or just site wide roles AND CASE WHEN $2 :: boolean THEN diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index da300763f643c..5486bb9f4afb4 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -6,8 +6,8 @@ FROM WHERE true -- @lookup_roles will filter for exact (role_name, org_id) pairs - AND CASE WHEN array_length(@lookup_roles :: name_organization_pair_list[], 1) > 0 THEN - (name, organization_id) ILIKE ANY (@lookup_roles::name_organization_pair_list[]) + AND CASE WHEN array_length(@lookup_roles :: name_organization_pair[], 1) > 0 THEN + (name, organization_id) = ANY (@lookup_roles::name_organization_pair[]) END -- This allows fetching all roles, or just site wide roles AND CASE WHEN @exclude_org_roles :: boolean THEN diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 3c9c6b24876ed..67d7e52b06b6d 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -29,7 +29,7 @@ sql: emit_all_enum_values: true overrides: # Used in 'CustomRoles' query to filter by (name,organization_id) - - db_type: "name_organization_pair_list" + - db_type: "name_organization_pair" go_type: type: "NameOrganizationPair" - column: "custom_roles.site_permissions" diff --git a/coderd/database/types.go b/coderd/database/types.go index bbb4e268011c3..564a352242d3f 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -154,11 +154,11 @@ func (a *NameOrganizationPair) Scan(_ interface{}) error { return xerrors.Errorf("this should never happen, type 'NameOrganizationPair' should only be used as a parameter") } -func (a NameOrganizationPair) Value() (driver.Value, error) { +func (a *NameOrganizationPair) Value() (driver.Value, error) { var orgID interface{} = a.OrganizationID if a.OrganizationID == uuid.Nil { orgID = nil } - return []interface{}{a.Name, orgID}, nil + return json.Marshal([]interface{}{a.Name, orgID}) } From 7f37232658940086c0a9e607c90af4fc86037156 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jun 2024 18:36:03 -0500 Subject: [PATCH 3/7] got it working --- coderd/database/db_test.go | 12 ++++ coderd/database/dbtestutil/postgres.go | 1 + coderd/database/dump.sql | 2 +- .../000215_scoped_org_db_roles.up.sql | 2 - ...000216_custom_role_pair_parameter.down.sql | 1 + .../000216_custom_role_pair_parameter.up.sql | 1 + coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/querier_test.go | 71 +++++++++++++------ coderd/database/queries.sql.go | 9 ++- coderd/database/queries/roles.sql | 8 ++- coderd/database/sqlc.yaml | 3 + coderd/database/types.go | 10 +-- 13 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 coderd/database/migrations/000216_custom_role_pair_parameter.down.sql create mode 100644 coderd/database/migrations/000216_custom_role_pair_parameter.up.sql diff --git a/coderd/database/db_test.go b/coderd/database/db_test.go index db7fe41eea3dc..fe664fb689377 100644 --- a/coderd/database/db_test.go +++ b/coderd/database/db_test.go @@ -5,10 +5,14 @@ package database_test import ( "context" "database/sql" + "os" "testing" "github.com/google/uuid" "github.com/lib/pq" + "github.com/rs/zerolog" + sqldblogger "github.com/simukti/sqldb-logger" + "github.com/simukti/sqldb-logger/logadapter/zerologadapter" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/database" @@ -92,6 +96,14 @@ func testSQLDB(t testing.TB) *sql.DB { t.Cleanup(closeFn) db, err := sql.Open("postgres", connection) + loggerAdapter := zerologadapter.New(zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})) + db = sqldblogger.OpenDriver(connection, db.Driver(), loggerAdapter, + sqldblogger.WithMinimumLevel(sqldblogger.LevelTrace), + sqldblogger.WithPreparerLevel(sqldblogger.LevelTrace), // default: LevelInfo + sqldblogger.WithQueryerLevel(sqldblogger.LevelTrace), // default: LevelInfo + sqldblogger.WithExecerLevel(sqldblogger.LevelTrace), // default: LevelInfo + ) + require.NoError(t, err) t.Cleanup(func() { _ = db.Close() }) diff --git a/coderd/database/dbtestutil/postgres.go b/coderd/database/dbtestutil/postgres.go index 33e0350821099..3a559778b6968 100644 --- a/coderd/database/dbtestutil/postgres.go +++ b/coderd/database/dbtestutil/postgres.go @@ -28,6 +28,7 @@ func Open() (string, func(), error) { if err != nil { return "", nil, xerrors.Errorf("connect to ci postgres: %w", err) } + defer db.Close() dbName, err := cryptorand.StringCharset(cryptorand.Lower, 10) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index dce976dbef622..9ec634cf79a2d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -75,7 +75,7 @@ COMMENT ON TYPE login_type IS 'Specifies the method of authentication. "none" is CREATE TYPE name_organization_pair AS ( name text, - organiztion_id uuid + organization_id uuid ); CREATE TYPE parameter_destination_scheme AS ENUM ( diff --git a/coderd/database/migrations/000215_scoped_org_db_roles.up.sql b/coderd/database/migrations/000215_scoped_org_db_roles.up.sql index bb959a832a46a..aecd19b8da668 100644 --- a/coderd/database/migrations/000215_scoped_org_db_roles.up.sql +++ b/coderd/database/migrations/000215_scoped_org_db_roles.up.sql @@ -5,5 +5,3 @@ ALTER TABLE ONLY organization_members ALTER COLUMN roles SET DEFAULT '{}'; -- No one should be using organization roles yet. If they are, the names in the -- database are now incorrect. Just remove them all. UPDATE organization_members SET roles = '{}'; - -CREATE TYPE name_organization_pair AS (name text, organiztion_id uuid); diff --git a/coderd/database/migrations/000216_custom_role_pair_parameter.down.sql b/coderd/database/migrations/000216_custom_role_pair_parameter.down.sql new file mode 100644 index 0000000000000..7322a09ee26b8 --- /dev/null +++ b/coderd/database/migrations/000216_custom_role_pair_parameter.down.sql @@ -0,0 +1 @@ +DROP TYPE name_organization_pair; diff --git a/coderd/database/migrations/000216_custom_role_pair_parameter.up.sql b/coderd/database/migrations/000216_custom_role_pair_parameter.up.sql new file mode 100644 index 0000000000000..b131054fc8dfb --- /dev/null +++ b/coderd/database/migrations/000216_custom_role_pair_parameter.up.sql @@ -0,0 +1 @@ +CREATE TYPE name_organization_pair AS (name text, organization_id uuid); diff --git a/coderd/database/models.go b/coderd/database/models.go index e5ba9fcea6841..544e8eba29625 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 6e2b1ff60cfdf..b9872c0ba13c4 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 4e667261addc6..1e1db7ca69d46 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -539,11 +539,10 @@ func TestReadCustomRoles(t *testing.T) { roles := make([]database.CustomRole, 0) for i := 0; i < 15; i++ { orgID := uuid.NullUUID{} - if i%7 != 0 { - orgID = uuid.NullUUID{ - UUID: orgIDs[i%len(orgIDs)], - Valid: true, - } + + orgID = uuid.NullUUID{ + UUID: orgIDs[i%len(orgIDs)], + Valid: true, } role, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{ @@ -561,25 +560,57 @@ func TestReadCustomRoles(t *testing.T) { testCases := []struct { Name string - Params database.CustomRoles2Params + Params database.CustomRolesParams Match func(role database.CustomRole) bool }{ + { + Name: "NilRoles", + Params: database.CustomRolesParams{ + LookupRoles: nil, + ExcludeOrgRoles: false, + OrganizationID: uuid.UUID{}, + }, + Match: func(role database.CustomRole) bool { + return true + }, + }, { // Empty params should return all roles - Name: "Empty", - Params: database.CustomRoles2Params{}, + Name: "Empty", + Params: database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{}, + ExcludeOrgRoles: false, + OrganizationID: uuid.UUID{}, + }, Match: func(role database.CustomRole) bool { return true }, }, - //{ - // // Only an organization roles - // Name: "Organization", - // Params: database.CustomRolesParams{}, - // Match: func(role database.CustomRole) bool { - // return true - // }, - //}, + { + Name: "Organization", + Params: database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{}, + ExcludeOrgRoles: false, + OrganizationID: orgIDs[1], + }, + Match: func(role database.CustomRole) bool { + return role.OrganizationID.UUID == orgIDs[1] + }, + }, + { + Name: "SpecificRole", + Params: database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{ + { + Name: roles[0].Name, + OrganizationID: roles[0].OrganizationID.UUID, + }, + }, + }, + Match: func(role database.CustomRole) bool { + return role.Name == roles[0].Name && role.OrganizationID.UUID == roles[0].OrganizationID.UUID + }, + }, } for _, tc := range testCases { @@ -588,8 +619,9 @@ func TestReadCustomRoles(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { t.Parallel() + t.Log(tc.Params) ctx := testutil.Context(t, testutil.WaitLong) - found, err := db.CustomRoles2(ctx, tc.Params) + found, err := db.CustomRoles(ctx, tc.Params) require.NoError(t, err) filtered := make([]database.CustomRole, 0) for _, role := range roles { @@ -599,9 +631,8 @@ func TestReadCustomRoles(t *testing.T) { } a := db2sdk.List(filtered, normalizedRoleName) - var _, _ = found, a - //b := db2sdk.List(found, normalizedRoleName) - //require.Equal(t, a, b) + b := db2sdk.List(found, normalizedRoleName) + require.Equal(t, a, b) }) } } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index dbec0d4374791..8201c977abdab 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -5605,17 +5605,20 @@ FROM WHERE true -- @lookup_roles will filter for exact (role_name, org_id) pairs + -- To do this manually in SQL, you can construct an array and cast it: + -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) AND CASE WHEN array_length($1 :: name_organization_pair[], 1) > 0 THEN (name, organization_id) = ANY ($1::name_organization_pair[]) + ELSE true END -- This allows fetching all roles, or just site wide roles AND CASE WHEN $2 :: boolean THEN - organization_id IS null + organization_id IS null OR true ELSE true END -- Allows fetching all roles to a particular organization AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - organization_id = $3 + organization_id = $3 OR true ELSE true END ` diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index 5486bb9f4afb4..5bda768b7455a 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -6,21 +6,25 @@ FROM WHERE true -- @lookup_roles will filter for exact (role_name, org_id) pairs + -- To do this manually in SQL, you can construct an array and cast it: + -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) AND CASE WHEN array_length(@lookup_roles :: name_organization_pair[], 1) > 0 THEN (name, organization_id) = ANY (@lookup_roles::name_organization_pair[]) + ELSE true END -- This allows fetching all roles, or just site wide roles AND CASE WHEN @exclude_org_roles :: boolean THEN - organization_id IS null + organization_id IS null OR true ELSE true END -- Allows fetching all roles to a particular organization AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - organization_id = @organization_id + organization_id = @organization_id OR true ELSE true END ; + -- name: UpsertCustomRole :one INSERT INTO custom_roles ( diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 67d7e52b06b6d..2db1644e06280 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -29,6 +29,9 @@ sql: emit_all_enum_values: true overrides: # Used in 'CustomRoles' query to filter by (name,organization_id) + - db_type: "name_organization_pairs" + go_type: + type: "NameOrganizationPairs" - db_type: "name_organization_pair" go_type: type: "NameOrganizationPair" diff --git a/coderd/database/types.go b/coderd/database/types.go index 564a352242d3f..7d24446ecad0c 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -3,6 +3,7 @@ package database import ( "database/sql/driver" "encoding/json" + "fmt" "time" "github.com/google/uuid" @@ -150,15 +151,14 @@ type NameOrganizationPair struct { OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` } -func (a *NameOrganizationPair) Scan(_ interface{}) error { +func (*NameOrganizationPair) Scan(_ interface{}) error { return xerrors.Errorf("this should never happen, type 'NameOrganizationPair' should only be used as a parameter") } -func (a *NameOrganizationPair) Value() (driver.Value, error) { - var orgID interface{} = a.OrganizationID +func (a NameOrganizationPair) Value() (driver.Value, error) { if a.OrganizationID == uuid.Nil { - orgID = nil + return fmt.Sprintf(`('%s', NULL)`, a.Name), nil } - return json.Marshal([]interface{}{a.Name, orgID}) + return fmt.Sprintf(`(%s,%s)`, a.Name, a.OrganizationID.String()), nil } From a305e703a89bf7b1a71a7f2b70ea1780966eef13 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jun 2024 18:36:35 -0500 Subject: [PATCH 4/7] revert logging --- coderd/database/db_test.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/coderd/database/db_test.go b/coderd/database/db_test.go index fe664fb689377..db7fe41eea3dc 100644 --- a/coderd/database/db_test.go +++ b/coderd/database/db_test.go @@ -5,14 +5,10 @@ package database_test import ( "context" "database/sql" - "os" "testing" "github.com/google/uuid" "github.com/lib/pq" - "github.com/rs/zerolog" - sqldblogger "github.com/simukti/sqldb-logger" - "github.com/simukti/sqldb-logger/logadapter/zerologadapter" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/database" @@ -96,14 +92,6 @@ func testSQLDB(t testing.TB) *sql.DB { t.Cleanup(closeFn) db, err := sql.Open("postgres", connection) - loggerAdapter := zerologadapter.New(zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})) - db = sqldblogger.OpenDriver(connection, db.Driver(), loggerAdapter, - sqldblogger.WithMinimumLevel(sqldblogger.LevelTrace), - sqldblogger.WithPreparerLevel(sqldblogger.LevelTrace), // default: LevelInfo - sqldblogger.WithQueryerLevel(sqldblogger.LevelTrace), // default: LevelInfo - sqldblogger.WithExecerLevel(sqldblogger.LevelTrace), // default: LevelInfo - ) - require.NoError(t, err) t.Cleanup(func() { _ = db.Close() }) From 8da36017ea2ab411e90ca208f60eb483522a907b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jun 2024 18:53:16 -0500 Subject: [PATCH 5/7] fixups --- coderd/database/dbmem/dbmem.go | 2 +- coderd/database/querier_test.go | 129 +++++++++++++++++++++++++++--- coderd/database/queries.sql.go | 4 +- coderd/database/queries/roles.sql | 4 +- coderd/database/types.go | 14 +++- 5 files changed, 136 insertions(+), 17 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7d50cdbaac6e3..727f76077b023 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1187,7 +1187,7 @@ func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesPar role := role if len(arg.LookupRoles) > 0 { if !slices.ContainsFunc(arg.LookupRoles, func(pair database.NameOrganizationPair) bool { - if !strings.EqualFold(pair.Name, role.Name) { + if pair.Name != role.Name { return false } diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 1e1db7ca69d46..0d523c25290e2 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -527,6 +527,7 @@ func TestReadCustomRoles(t *testing.T) { sqlDB := testSQLDB(t) err := migrations.Up(sqlDB) require.NoError(t, err) + db := database.New(sqlDB) ctx := testutil.Context(t, testutil.WaitLong) @@ -536,21 +537,30 @@ func TestReadCustomRoles(t *testing.T) { orgIDs[i] = uuid.New() } - roles := make([]database.CustomRole, 0) + allRoles := make([]database.CustomRole, 0) + siteRoles := make([]database.CustomRole, 0) + orgRoles := make([]database.CustomRole, 0) for i := 0; i < 15; i++ { - orgID := uuid.NullUUID{} - - orgID = uuid.NullUUID{ + orgID := uuid.NullUUID{ UUID: orgIDs[i%len(orgIDs)], Valid: true, } + if i%4 == 0 { + // Some should be site wide + orgID = uuid.NullUUID{} + } role, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{ Name: fmt.Sprintf("role-%d", i), OrganizationID: orgID, }) require.NoError(t, err) - roles = append(roles, role) + allRoles = append(allRoles, role) + if orgID.Valid { + orgRoles = append(orgRoles, role) + } else { + siteRoles = append(siteRoles, role) + } } // normalizedRoleName allows for the simple ElementsMatch to work properly. @@ -558,6 +568,13 @@ func TestReadCustomRoles(t *testing.T) { return role.Name + ":" + role.OrganizationID.UUID.String() } + roleToLookup := func(role database.CustomRole) database.NameOrganizationPair { + return database.NameOrganizationPair{ + Name: role.Name, + OrganizationID: role.OrganizationID.UUID, + } + } + testCases := []struct { Name string Params database.CustomRolesParams @@ -598,17 +615,108 @@ func TestReadCustomRoles(t *testing.T) { }, }, { - Name: "SpecificRole", + Name: "SpecificOrgRole", + Params: database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{ + { + Name: orgRoles[0].Name, + OrganizationID: orgRoles[0].OrganizationID.UUID, + }, + }, + }, + Match: func(role database.CustomRole) bool { + return role.Name == orgRoles[0].Name && role.OrganizationID.UUID == orgRoles[0].OrganizationID.UUID + }, + }, + { + Name: "SpecificSiteRole", + Params: database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{ + { + Name: siteRoles[0].Name, + OrganizationID: siteRoles[0].OrganizationID.UUID, + }, + }, + }, + Match: func(role database.CustomRole) bool { + return role.Name == siteRoles[0].Name && role.OrganizationID.UUID == siteRoles[0].OrganizationID.UUID + }, + }, + { + Name: "FewSpecificRoles", + Params: database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{ + { + Name: orgRoles[0].Name, + OrganizationID: orgRoles[0].OrganizationID.UUID, + }, + { + Name: orgRoles[1].Name, + OrganizationID: orgRoles[1].OrganizationID.UUID, + }, + { + Name: siteRoles[0].Name, + OrganizationID: siteRoles[0].OrganizationID.UUID, + }, + }, + }, + Match: func(role database.CustomRole) bool { + return (role.Name == orgRoles[0].Name && role.OrganizationID.UUID == orgRoles[0].OrganizationID.UUID) || + (role.Name == orgRoles[1].Name && role.OrganizationID.UUID == orgRoles[1].OrganizationID.UUID) || + (role.Name == siteRoles[0].Name && role.OrganizationID.UUID == siteRoles[0].OrganizationID.UUID) + }, + }, + { + Name: "AllRolesByLookup", + Params: database.CustomRolesParams{ + LookupRoles: db2sdk.List(allRoles, roleToLookup), + }, + Match: func(role database.CustomRole) bool { + return true + }, + }, + { + Name: "NotExists", Params: database.CustomRolesParams{ LookupRoles: []database.NameOrganizationPair{ { - Name: roles[0].Name, - OrganizationID: roles[0].OrganizationID.UUID, + Name: "not-exists", + OrganizationID: uuid.New(), + }, + { + Name: "not-exists", + OrganizationID: uuid.Nil, + }, + }, + }, + Match: func(role database.CustomRole) bool { + return false + }, + }, + { + Name: "Mixed", + Params: database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{ + { + Name: "not-exists", + OrganizationID: uuid.New(), + }, + { + Name: "not-exists", + OrganizationID: uuid.Nil, + }, + { + Name: orgRoles[0].Name, + OrganizationID: orgRoles[0].OrganizationID.UUID, + }, + { + Name: siteRoles[0].Name, }, }, }, Match: func(role database.CustomRole) bool { - return role.Name == roles[0].Name && role.OrganizationID.UUID == roles[0].OrganizationID.UUID + return (role.Name == orgRoles[0].Name && role.OrganizationID.UUID == orgRoles[0].OrganizationID.UUID) || + (role.Name == siteRoles[0].Name && role.OrganizationID.UUID == siteRoles[0].OrganizationID.UUID) }, }, } @@ -619,12 +727,11 @@ func TestReadCustomRoles(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { t.Parallel() - t.Log(tc.Params) ctx := testutil.Context(t, testutil.WaitLong) found, err := db.CustomRoles(ctx, tc.Params) require.NoError(t, err) filtered := make([]database.CustomRole, 0) - for _, role := range roles { + for _, role := range allRoles { if tc.Match(role) { filtered = append(filtered, role) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8201c977abdab..6a5d691ac423e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5613,12 +5613,12 @@ WHERE END -- This allows fetching all roles, or just site wide roles AND CASE WHEN $2 :: boolean THEN - organization_id IS null OR true + organization_id IS null ELSE true END -- Allows fetching all roles to a particular organization AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - organization_id = $3 OR true + organization_id = $3 ELSE true END ` diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index 5bda768b7455a..d98afe6f2c81e 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -14,12 +14,12 @@ WHERE END -- This allows fetching all roles, or just site wide roles AND CASE WHEN @exclude_org_roles :: boolean THEN - organization_id IS null OR true + organization_id IS null ELSE true END -- Allows fetching all roles to a particular organization AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - organization_id = @organization_id OR true + organization_id = @organization_id ELSE true END ; diff --git a/coderd/database/types.go b/coderd/database/types.go index 7d24446ecad0c..ea792eecf239d 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -155,9 +155,21 @@ func (*NameOrganizationPair) Scan(_ interface{}) error { return xerrors.Errorf("this should never happen, type 'NameOrganizationPair' should only be used as a parameter") } +// Value returns the tuple **literal** +// To get the literal value to return, you can use the expression syntax in a psql +// shell. +// +// SELECT ('customrole'::text,'ece79dac-926e-44ca-9790-2ff7c5eb6e0c'::uuid); +// To see 'null' option +// SELECT ('customrole',null); +// +// This value is usually used as an array, NameOrganizationPair[]. You can see +// what that literal is as well, with proper quoting. +// +// SELECT ARRAY[('customrole'::text,'ece79dac-926e-44ca-9790-2ff7c5eb6e0c'::uuid)]; func (a NameOrganizationPair) Value() (driver.Value, error) { if a.OrganizationID == uuid.Nil { - return fmt.Sprintf(`('%s', NULL)`, a.Name), nil + return fmt.Sprintf(`('%s',)`, a.Name), nil } return fmt.Sprintf(`(%s,%s)`, a.Name, a.OrganizationID.String()), nil From dc7fe77ce060bf890b3d1b4a128c1df24515dc4e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jun 2024 18:58:52 -0500 Subject: [PATCH 6/7] fixup null --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 5 +++-- coderd/database/queries/roles.sql | 3 ++- coderd/database/types.go | 8 ++------ 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index 544e8eba29625..e5ba9fcea6841 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index b9872c0ba13c4..6e2b1ff60cfdf 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 6a5d691ac423e..b7e4ed58d35cb 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database @@ -5608,7 +5608,8 @@ WHERE -- To do this manually in SQL, you can construct an array and cast it: -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) AND CASE WHEN array_length($1 :: name_organization_pair[], 1) > 0 THEN - (name, organization_id) = ANY ($1::name_organization_pair[]) + -- Using 'coalesce' to avoid troubles with null literals being an empty string. + (name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY ($1::name_organization_pair[]) ELSE true END -- This allows fetching all roles, or just site wide roles diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index d98afe6f2c81e..ec5566a3d0dbb 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -9,7 +9,8 @@ WHERE -- To do this manually in SQL, you can construct an array and cast it: -- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[]) AND CASE WHEN array_length(@lookup_roles :: name_organization_pair[], 1) > 0 THEN - (name, organization_id) = ANY (@lookup_roles::name_organization_pair[]) + -- Using 'coalesce' to avoid troubles with null literals being an empty string. + (name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY (@lookup_roles::name_organization_pair[]) ELSE true END -- This allows fetching all roles, or just site wide roles diff --git a/coderd/database/types.go b/coderd/database/types.go index ea792eecf239d..fd7a2fed82300 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -160,17 +160,13 @@ func (*NameOrganizationPair) Scan(_ interface{}) error { // shell. // // SELECT ('customrole'::text,'ece79dac-926e-44ca-9790-2ff7c5eb6e0c'::uuid); -// To see 'null' option -// SELECT ('customrole',null); +// To see 'null' option. Using the nil uuid as null to avoid empty string literals for null. +// SELECT ('customrole',00000000-0000-0000-0000-000000000000); // // This value is usually used as an array, NameOrganizationPair[]. You can see // what that literal is as well, with proper quoting. // // SELECT ARRAY[('customrole'::text,'ece79dac-926e-44ca-9790-2ff7c5eb6e0c'::uuid)]; func (a NameOrganizationPair) Value() (driver.Value, error) { - if a.OrganizationID == uuid.Nil { - return fmt.Sprintf(`('%s',)`, a.Name), nil - } - return fmt.Sprintf(`(%s,%s)`, a.Name, a.OrganizationID.String()), nil } From a1a42b878b622d10f19a8945dcea8b29da6a73e5 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jun 2024 19:00:37 -0500 Subject: [PATCH 7/7] fixup sqlc --- coderd/database/sqlc.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 2db1644e06280..67d7e52b06b6d 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -29,9 +29,6 @@ sql: emit_all_enum_values: true overrides: # Used in 'CustomRoles' query to filter by (name,organization_id) - - db_type: "name_organization_pairs" - go_type: - type: "NameOrganizationPairs" - db_type: "name_organization_pair" go_type: type: "NameOrganizationPair"