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" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy