Skip to content

Commit 4dab92a

Browse files
authored
fix(license): exclude service account users from seat count (onyx-dot-app#10053)
1 parent 7eb68d6 commit 4dab92a

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

backend/ee/onyx/db/license.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from onyx.auth.schemas import UserRole
1414
from onyx.cache.factory import get_cache_backend
1515
from onyx.configs.constants import ANONYMOUS_USER_EMAIL
16+
from onyx.db.enums import AccountType
1617
from onyx.db.models import License
1718
from onyx.db.models import User
1819
from onyx.utils.logger import setup_logger
@@ -107,12 +108,13 @@ def get_used_seats(tenant_id: str | None = None) -> int:
107108
Get current seat usage directly from database.
108109
109110
For multi-tenant: counts users in UserTenantMapping for this tenant.
110-
For self-hosted: counts all active users (excludes EXT_PERM_USER role
111-
and the anonymous system user).
111+
For self-hosted: counts all active users.
112112
113-
TODO: Exclude API key dummy users from seat counting. API keys create
114-
users with emails like `__DANSWER_API_KEY_*` that should not count toward
115-
seat limits. See: https://linear.app/onyx-app/issue/ENG-3518
113+
Only human accounts count toward seat limits.
114+
SERVICE_ACCOUNT (API key dummy users), EXT_PERM_USER, and the
115+
anonymous system user are excluded. BOT (Slack users) ARE counted
116+
because they represent real humans and get upgraded to STANDARD
117+
when they log in via web.
116118
"""
117119
if MULTI_TENANT:
118120
from ee.onyx.server.tenants.user_mapping import get_tenant_count
@@ -129,6 +131,7 @@ def get_used_seats(tenant_id: str | None = None) -> int:
129131
User.is_active == True, # type: ignore # noqa: E712
130132
User.role != UserRole.EXT_PERM_USER,
131133
User.email != ANONYMOUS_USER_EMAIL, # type: ignore
134+
User.account_type != AccountType.SERVICE_ACCOUNT,
132135
)
133136
)
134137
return result.scalar() or 0

backend/tests/unit/ee/onyx/db/test_license.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ee.onyx.db.license import check_seat_availability
1010
from ee.onyx.db.license import delete_license
1111
from ee.onyx.db.license import get_license
12+
from ee.onyx.db.license import get_used_seats
1213
from ee.onyx.db.license import upsert_license
1314
from ee.onyx.server.license.models import LicenseMetadata
1415
from ee.onyx.server.license.models import LicenseSource
@@ -214,3 +215,43 @@ def test_seats_full_multi_tenant(
214215
assert result.available is False
215216
assert result.error_message is not None
216217
mock_tenant_count.assert_called_once_with("tenant-abc")
218+
219+
220+
class TestGetUsedSeatsAccountTypeFiltering:
221+
"""Verify get_used_seats query excludes SERVICE_ACCOUNT but includes BOT."""
222+
223+
@patch("ee.onyx.db.license.MULTI_TENANT", False)
224+
@patch("onyx.db.engine.sql_engine.get_session_with_current_tenant")
225+
def test_excludes_service_accounts(self, mock_get_session: MagicMock) -> None:
226+
"""SERVICE_ACCOUNT users should not count toward seats."""
227+
mock_session = MagicMock()
228+
mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
229+
mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
230+
mock_session.execute.return_value.scalar.return_value = 5
231+
232+
result = get_used_seats()
233+
234+
assert result == 5
235+
# Inspect the compiled query to verify account_type filter
236+
call_args = mock_session.execute.call_args
237+
query = call_args[0][0]
238+
compiled = str(query.compile(compile_kwargs={"literal_binds": True}))
239+
assert "SERVICE_ACCOUNT" in compiled
240+
# BOT should NOT be excluded
241+
assert "BOT" not in compiled
242+
243+
@patch("ee.onyx.db.license.MULTI_TENANT", False)
244+
@patch("onyx.db.engine.sql_engine.get_session_with_current_tenant")
245+
def test_still_excludes_ext_perm_user(self, mock_get_session: MagicMock) -> None:
246+
"""EXT_PERM_USER exclusion should still be present."""
247+
mock_session = MagicMock()
248+
mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
249+
mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
250+
mock_session.execute.return_value.scalar.return_value = 3
251+
252+
get_used_seats()
253+
254+
call_args = mock_session.execute.call_args
255+
query = call_args[0][0]
256+
compiled = str(query.compile(compile_kwargs={"literal_binds": True}))
257+
assert "EXT_PERM_USER" in compiled

0 commit comments

Comments
 (0)