Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions server/cmd/gram/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,6 @@ func newStartCommand() *cli.Command {
rbacEnabled,
challengeLoggingEnabled,
roleClient,
cache.NewRedisCacheAdapter(redisClient),
authz.EngineOpts{DevMode: c.String("environment") == "local"},
)

Expand Down Expand Up @@ -945,7 +944,8 @@ func newStartCommand() *cli.Command {

about.Attach(mux, about.NewService(logger, tracerProvider))
external.AttachWebhookHandler(mux, external.NewWebhookHandler(logger, tracerProvider, newWorkOSWebhooksClient(c), temporalEnv))
access.Attach(mux, access.NewService(logger, tracerProvider, db, chDB, sessionManager, roleClient, authzEngine, productFeatures, auditLogger))
roleManager := access.NewRoleManager(logger, db, roleClient, auditLogger)
access.Attach(mux, access.NewService(logger, tracerProvider, db, chDB, sessionManager, roleManager, authzEngine, productFeatures, auditLogger))
assistants.Attach(mux, assistantsSvc)
assistantmemories.Attach(mux, assistantmemories.NewService(
logger,
Expand Down
1 change: 0 additions & 1 deletion server/cmd/gram/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,6 @@ func newWorkerCommand() *cli.Command {
rbacEnabled,
challengeLoggingEnabled,
workos.NewStubClient(),
cache.NewRedisCacheAdapter(redisClient),
authz.EngineOpts{DevMode: c.String("environment") == "local"},
)

Expand Down
90 changes: 49 additions & 41 deletions server/internal/access/createrole_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ func TestService_CreateRole(t *testing.T) {
CreatedAt: mockRoleTimestamp,
UpdatedAt: mockRoleTimestamp,
}, nil).Once()
ti.roles.On("ListMembers", mock.Anything, mockidp.MockOrgID).Return([]thirdpartyworkos.Member{
mockMember(mockidp.MockOrgID, "membership_1", "user_1", "member"),
mockMember(mockidp.MockOrgID, "membership_2", "user_2", "member"),
// user_workos_only has never logged into Gram — should not be counted
mockMember(mockidp.MockOrgID, "membership_workos_only", "user_workos_only", "member"),
}, nil).Once()
ti.roles.On("UpdateMemberRole", mock.Anything, "membership_1", "org-custom-builder").Return(&thirdpartyworkos.Member{
ID: "membership_1",
UserID: "user_1",
Expand All @@ -60,8 +54,12 @@ func TestService_CreateRole(t *testing.T) {
CreatedAt: mockMembershipTimestamp,
}, nil).Once()

seedRole(t, ctx, ti.conn, authCtx.ActiveOrganizationID, mockSystemRole("role_member", "Member", authz.SystemRoleMember))
seedConnectedUser(t, ctx, ti.conn, authCtx.ActiveOrganizationID, "local_user_1", "user1@test.com", "User 1", "user_1", "membership_1")
seedConnectedUser(t, ctx, ti.conn, authCtx.ActiveOrganizationID, "local_user_2", "user2@test.com", "User 2", "user_2", "membership_2")
seedRoleAssignment(t, ctx, ti.conn, authCtx.ActiveOrganizationID, "local_user_1", mockMember("", "membership_1", "user_1", authz.SystemRoleMember))
seedRoleAssignment(t, ctx, ti.conn, authCtx.ActiveOrganizationID, "local_user_2", mockMember("", "membership_2", "user_2", authz.SystemRoleMember))
seedRoleAssignment(t, ctx, ti.conn, authCtx.ActiveOrganizationID, "", mockMember("", "membership_workos_only", "user_workos_only", authz.SystemRoleMember))

role, err := ti.service.CreateRole(ctx, &gen.CreateRolePayload{
Name: "Custom Builder",
Expand All @@ -74,14 +72,19 @@ func TestService_CreateRole(t *testing.T) {
})
require.NoError(t, err)
require.Equal(t, "Custom Builder", role.Name)
require.NotEmpty(t, role.ID)
require.NotEqual(t, "role_1", role.ID)
require.Equal(t, "Can build selected resources", role.Description)
require.False(t, role.IsSystem)
require.Equal(t, 2, role.MemberCount)
require.Equal(t, mockRoleTimestamp, role.CreatedAt)
require.Equal(t, mockRoleTimestamp, role.UpdatedAt)
require.NotEmpty(t, role.CreatedAt)
require.NotEmpty(t, role.UpdatedAt)
require.Len(t, role.Grants, 2)
roundtrip, err := ti.service.GetRole(ctx, &gen.GetRolePayload{ID: role.ID})
require.NoError(t, err)
require.Equal(t, role.ID, roundtrip.ID)

grants := listPrincipalGrants(t, ctx, ti.conn, authCtx.ActiveOrganizationID, urn.NewPrincipal(urn.PrincipalTypeRole, "org-custom-builder"))
grants := listPrincipalGrants(t, ctx, ti.conn, authCtx.ActiveOrganizationID, urn.NewPrincipal(urn.PrincipalTypeRole, "organization:"+role.ID))
require.Len(t, grants, 3)
}

Expand All @@ -93,33 +96,53 @@ func TestService_CreateRole_WorkOSCreateFailure(t *testing.T) {
Name: "Custom Builder",
Slug: "org-custom-builder",
Description: "Can build selected resources",
}).Return((*thirdpartyworkos.Role)(nil), errors.New("workos unavailable")).Once()
}).Return((*thirdpartyworkos.Role)(nil), errors.New("workos unavailable")).Times(3)

_, err := ti.service.CreateRole(ctx, &gen.CreateRolePayload{
role, err := ti.service.CreateRole(ctx, &gen.CreateRolePayload{
Name: "Custom Builder",
Description: "Can build selected resources",
Grants: []*gen.RoleGrant{
{Scope: string(authz.ScopeProjectRead), Selectors: []*gen.Selector{{ResourceKind: "project", ResourceID: "project-1"}}},
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "create role in workos")
require.NoError(t, err)
require.Equal(t, "Custom Builder", role.Name)
}

func TestService_CreateRole_ContinuesAfterConflictWhenRoleAlreadyExists(t *testing.T) {
func TestService_CreateRole_WorkOSConflictFailure(t *testing.T) {
t.Parallel()

ctx, ti := newTestAccessService(t)
ti.roles.On("CreateRole", mock.Anything, mockidp.MockOrgID, thirdpartyworkos.CreateRoleOpts{
Name: "Custom Builder",
Slug: "org-custom-builder",
Description: "Can build selected resources",
}).Return((*thirdpartyworkos.Role)(nil), &thirdpartyworkos.APIError{Method: "POST", Path: "/authorization/organizations/org_workos_test/roles", StatusCode: 409, Body: "role already exists"}).Once()

role, err := ti.service.CreateRole(ctx, &gen.CreateRolePayload{
Name: "Custom Builder",
Description: "Can build selected resources",
Grants: []*gen.RoleGrant{
{Scope: string(authz.ScopeProjectRead), Selectors: []*gen.Selector{{ResourceKind: "project", ResourceID: "project-1"}}},
},
})
require.NoError(t, err)
require.Equal(t, "Custom Builder", role.Name)
}

func TestService_CreateRole_WorkOSConflictUsesLocalRole(t *testing.T) {
t.Parallel()

ctx, ti := newTestAccessService(t)
authCtx, ok := contextvalues.GetAuthContext(ctx)
require.True(t, ok)
require.NotNil(t, authCtx)
existingRole := mockRole("role_existing", "Custom Builder", "org-custom-builder", "Can build selected resources")

roleID := seedRole(t, ctx, ti.conn, authCtx.ActiveOrganizationID, mockRole("role_1", "Custom Builder", "org-custom-builder", "Can build selected resources"))
ti.roles.On("CreateRole", mock.Anything, mockidp.MockOrgID, thirdpartyworkos.CreateRoleOpts{
Name: "Custom Builder",
Slug: "org-custom-builder",
Description: "Can build selected resources",
}).Return((*thirdpartyworkos.Role)(nil), &thirdpartyworkos.APIError{Method: "POST", Path: "/authorization/organizations/org_workos_test/roles", StatusCode: 409, Body: "role already exists"}).Once()
ti.roles.On("ListRoles", mock.Anything, mockidp.MockOrgID).Return([]thirdpartyworkos.Role{existingRole}, nil).Once()

role, err := ti.service.CreateRole(ctx, &gen.CreateRolePayload{
Name: "Custom Builder",
Expand All @@ -129,12 +152,8 @@ func TestService_CreateRole_ContinuesAfterConflictWhenRoleAlreadyExists(t *testi
},
})
require.NoError(t, err)
require.Equal(t, "role_existing", role.ID)
require.Equal(t, roleID, role.ID)
require.Equal(t, "Custom Builder", role.Name)

grants := listPrincipalGrants(t, ctx, ti.conn, authCtx.ActiveOrganizationID, urn.NewPrincipal(urn.PrincipalTypeRole, "org-custom-builder"))
require.Len(t, grants, 1)
require.Equal(t, authCtx.ActiveOrganizationID, grants[0].OrganizationID)
}

func TestService_CreateRole_RejectsEmptySlug(t *testing.T) {
Expand Down Expand Up @@ -198,43 +217,32 @@ func TestService_CreateRole_AuditLog(t *testing.T) {
require.Equal(t, beforeCount+1, afterCount)
}

func TestService_CreateRole_GrantSyncFailureDoesNotAssignMembers(t *testing.T) {
func TestService_CreateRole_LocalRoleWriteFailureDoesNotAssignMembers(t *testing.T) {
t.Parallel()

ctx, ti := newTestAccessService(t)
authCtx, ok := contextvalues.GetAuthContext(ctx)
require.True(t, ok)
require.NotNil(t, authCtx)

ti.roles.On("CreateRole", mock.Anything, mockidp.MockOrgID, thirdpartyworkos.CreateRoleOpts{
Name: "Broken Builder",
Slug: "org-broken-builder",
Description: "Will fail grant sync",
}).Run(func(mock.Arguments) {
ti.conn.Close()
}).Return(&thirdpartyworkos.Role{
ID: "role_1",
Name: "Broken Builder",
Slug: "org-broken-builder",
Description: "Will fail grant sync",
CreatedAt: mockRoleTimestamp,
UpdatedAt: mockRoleTimestamp,
}, nil).Once()

inspectConn, err := pgxpool.New(ctx, ti.conn.Config().ConnString())
require.NoError(t, err)
t.Cleanup(inspectConn.Close)

_, err = ti.service.CreateRole(ctx, &gen.CreateRolePayload{
ti.conn.Close()
_, err = ti.service.roleMgr.CreateRole(ctx, authCtx.ActiveOrganizationID, mockidp.MockOrgID, accessAuditActor{
Principal: urn.NewPrincipal(urn.PrincipalTypeUser, authCtx.UserID),
DisplayName: authCtx.Email,
}, &gen.CreateRolePayload{
Name: "Broken Builder",
Description: "Will fail grant sync",
Description: "Will fail local write",
Grants: []*gen.RoleGrant{
{Scope: string(authz.ScopeProjectRead), Selectors: []*gen.Selector{{ResourceKind: "project", ResourceID: "project-1"}}},
},
MemberIds: []string{"local_user_1", "local_user_2"},
})
require.Error(t, err)
require.Contains(t, err.Error(), "sync grants for created role")
require.Contains(t, err.Error(), "role transaction")

grants, err := accessrepo.New(inspectConn).ListPrincipalGrantsByOrg(ctx, accessrepo.ListPrincipalGrantsByOrgParams{
OrganizationID: authCtx.ActiveOrganizationID,
Expand Down
Loading
Loading