You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
FrontierService.GetOrganization returns 500 internal when you look up a disabled org by its slug (name). The same org returns 200 when you look it up by id. This is a user facing RPC (frontier.proto), so it should never answer a known, expected condition with an internal error.
Behavior today
Called as a platform superuser against a disabled org:
Lookup
HTTP
connect code
body
disabled org by slug
500
internal
{"code":"internal","message":"internal server error"}
disabled org by id
200
-
returns the org (state: disabled)
enabled org by slug
200
-
returns the org
unknown slug
404
not_found
-
unknown uuid
403
permission_denied
-
So the same disabled org gives 200 by id but 500 by slug. That is both a wrong status code and an inconsistency between the two ways to address the same org.
Why it matters
GetOrganization is on FrontierService and is user facing. A 500 internal tells clients "server bug" and is the wrong signal for a normal state (the org is disabled). Clients cannot handle it cleanly.
It leaks an internal error string path (handleAuthErr: org is disabled) instead of a typed, documented error.
It breaks slug based deep links for disabled orgs. The admin UI slug URL work (feat(admin): show org slug in detail URLs instead of UUID #1763) resolves the URL segment through GetOrganization. A disabled org opened by slug (refresh or bookmark) hits this 500 and the page cannot load.
Root cause
The RPC handler itself is fine. internal/api/v1beta1connect/organization.goGetOrganization calls orgService.GetRaw, which returns disabled orgs.
The 500 comes from the authorization step that runs before the handler:
pkg/server/connect_interceptors/authorization.go maps GetOrganization to handler.IsAuthorized(Object{Namespace: organization, ID: req.GetId()}, GetPermission, req). req.GetId() is the raw URL segment, which can be a slug.
internal/api/v1beta1connect/authorize.goIsAuthorized calls resourceService.CheckAuthz(...). Resolving the org by name rejects disabled orgs and returns organization.ErrDisabled. Resolving by id reads the graph directly and passes (disabled orgs stay authorized by id on purpose, per the Disable service comment in core/organization/service.go).
handleAuthErr maps organization.ErrNotExist to CodeNotFound, but organization.ErrDisabled is not handled and falls through to the default branch, which returns CodeInternal.
No 500 for this case. Pick one, consistently for both id and slug:
Preferred: let the authz name lookup treat disabled orgs the same as the id path, so GetOrganization returns the disabled org for both id and slug. This matches the admin use case (a superuser can already read a disabled org by id) and removes the slug vs id split.
If reading a disabled org through this endpoint should be blocked, return a typed client error (for example failed_precondition "org is disabled" or not_found) for both id and slug, and stop returning the org by id too. Today the two paths disagree.
At minimum, handleAuthErr should recognize organization.ErrDisabled and map it to a proper client code instead of CodeInternal, so the endpoint never returns a 500 for a disabled org.
Steps to reproduce
Create an org, then disable it (DisableOrganization).
Call GetOrganization with {"id": "<slug>"} as a superuser. Returns 500 internal.
Call GetOrganization with {"id": "<uuid>"} for the same org. Returns 200 with state: disabled.
Related
Same pattern of user facing endpoints returning 500 instead of a proper client code: #1693, #1697.
Summary
FrontierService.GetOrganizationreturns500 internalwhen you look up a disabled org by its slug (name). The same org returns200when you look it up byid. This is a user facing RPC (frontier.proto), so it should never answer a known, expected condition with an internal error.Behavior today
Called as a platform superuser against a disabled org:
internal{"code":"internal","message":"internal server error"}state: disabled)not_foundpermission_deniedSo the same disabled org gives
200by id but500by slug. That is both a wrong status code and an inconsistency between the two ways to address the same org.Why it matters
GetOrganizationis onFrontierServiceand is user facing. A500 internaltells clients "server bug" and is the wrong signal for a normal state (the org is disabled). Clients cannot handle it cleanly.handleAuthErr: org is disabled) instead of a typed, documented error.GetOrganization. A disabled org opened by slug (refresh or bookmark) hits this 500 and the page cannot load.Root cause
The RPC handler itself is fine.
internal/api/v1beta1connect/organization.goGetOrganizationcallsorgService.GetRaw, which returns disabled orgs.The 500 comes from the authorization step that runs before the handler:
pkg/server/connect_interceptors/authorization.gomapsGetOrganizationtohandler.IsAuthorized(Object{Namespace: organization, ID: req.GetId()}, GetPermission, req).req.GetId()is the raw URL segment, which can be a slug.internal/api/v1beta1connect/authorize.goIsAuthorizedcallsresourceService.CheckAuthz(...). Resolving the org by name rejects disabled orgs and returnsorganization.ErrDisabled. Resolving by id reads the graph directly and passes (disabled orgs stay authorized by id on purpose, per theDisableservice comment incore/organization/service.go).handleAuthErrmapsorganization.ErrNotExisttoCodeNotFound, butorganization.ErrDisabledis not handled and falls through to thedefaultbranch, which returnsCodeInternal.Expected behavior
No
500for this case. Pick one, consistently for both id and slug:GetOrganizationreturns the disabled org for both id and slug. This matches the admin use case (a superuser can already read a disabled org by id) and removes the slug vs id split.failed_precondition"org is disabled" ornot_found) for both id and slug, and stop returning the org by id too. Today the two paths disagree.At minimum,
handleAuthErrshould recognizeorganization.ErrDisabledand map it to a proper client code instead ofCodeInternal, so the endpoint never returns a 500 for a disabled org.Steps to reproduce
DisableOrganization).GetOrganizationwith{"id": "<slug>"}as a superuser. Returns500 internal.GetOrganizationwith{"id": "<uuid>"}for the same org. Returns200withstate: disabled.Related
Same pattern of user facing endpoints returning
500instead of a proper client code: #1693, #1697.