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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ Restricted strictly to read-only views and review execution.
- **Subscriptions**:
- `GET /api/v1/subscriptions/:id` ➔ `subscriptionsHandler.GetSubscription`
- `GET /api/v1/subscriptions/current` ➔ `subscriptionsHandler.GetCurrentSubscription`
- **GitHub App** (a deliberate exception to read-only: anyone who can access a GitHub account on GitHub may connect it to their org; disconnecting stays owner-only):
- `POST /api/v1/github-app/start` ➔ `s.GitHubAppConnectStart`
- `POST /api/v1/github-app/installations/:connector_id/connect` ➔ `s.GitHubAppConnect`

#### Dynamic Role Checks & Middlewares

Expand Down
5 changes: 5 additions & 0 deletions config/osv-scanner.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
[[ignoredVulns]]
id = "GO-2026-5932"
reason = "False positive — golang.org/x/crypto/openpgp sub-package is never imported by this project; only golang.org/x/crypto/bcrypt is used."

[[ignoredVulns]]
id = "GHSA-gcqx-w48m-w2h4"
ignoreUntil = 2027-01-28
reason = "No patched release exists. deepmerge 4.3.1 is only pulled in by dev tooling (jest, fork-ts-checker-webpack-plugin); `npm ls deepmerge --omit=dev` is empty, so it never ships and never merges untrusted input. Re-check when a fixed version is published."
1 change: 1 addition & 0 deletions docs/audit-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Generated from `internal/auditlog/catalog.go`.
| `chat.file.upload` | `chat` | file | info | Chat file uploaded | A file was attached to a chat. |
| `connector.create` | `connector` | connector | warning | Integration connected | A source-control integration was connected. |
| `connector.delete` | `connector` | connector | warning | Integration removed | A source-control integration was removed. |
| `connector.github_app.start` | `connector` | connector | info | GitHub App connection started | A member started connecting a GitHub account or organization through the GitHub App. |
| `connector.reconnect` | `connector` | connector | info | Integration reconnected | A source-control integration was reconnected. |
| `connector.update` | `connector` | connector | warning | Integration updated | A source-control integration was updated. |
| `export.audit` | `export` | audit | warning | Audit log exported | Audit events were exported. |
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gitleaks/go-gitdiff v0.9.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.6 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
Expand Down Expand Up @@ -257,7 +257,7 @@ require (
golang.org/x/arch v0.25.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.61.0 // indirect
golang.org/x/net v0.62.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.24.0 // indirect
golang.org/x/sys v0.49.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4=
github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand Down Expand Up @@ -713,6 +715,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down
4 changes: 2 additions & 2 deletions internal/api/audit_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ func isAuthAuditSkipped(path string) bool {
// audited registers a route and its audit spec atomically, so the two cannot
// drift. It returns the Echo route (with the full path) for callers that need
// it.
func (s *Server) audited(g *echo.Group, method, path string, spec auditlog.Spec, h echo.HandlerFunc) *echo.Route {
r := g.Add(method, path, h)
func (s *Server) audited(g *echo.Group, method, path string, spec auditlog.Spec, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
r := g.Add(method, path, h, m...)
if s.auditRegistry != nil {
s.auditRegistry.Register(method, r.Path, spec)
}
Expand Down
37 changes: 34 additions & 3 deletions internal/api/auth/middleware.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package auth

import (
"context"
"database/sql"
"fmt"
"net/http"
Expand Down Expand Up @@ -252,11 +253,20 @@ func (am *AuthMiddleware) EnforceSubscriptionLimits() echo.MiddlewareFunc {
}

if err := loadPlanState(); err != nil {
if err == sql.ErrNoRows {
if err != sql.ErrNoRows {
fmt.Printf("[Subscription] plan lookup failed user=%d org=%d err=%v\n", user.ID, orgID, err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to check subscription")
}
// Not a member: only a super admin may continue.
plan, isSuperAdmin, err := am.loadSuperAdminPlan(reqCtx, user.ID, orgID)
if err != nil {
fmt.Printf("[Subscription] super admin plan lookup failed user=%d org=%d err=%v\n", user.ID, orgID, err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to check subscription")
}
if !isSuperAdmin {
return echo.NewHTTPError(http.StatusForbidden, "no access to this organization")
}
fmt.Printf("[Subscription] plan lookup failed user=%d org=%d err=%v\n", user.ID, orgID, err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to check subscription")
planType = plan
}

// Resolve plan type safely (NULL or empty -> free)
Expand All @@ -280,6 +290,27 @@ func (am *AuthMiddleware) EnforceSubscriptionLimits() echo.MiddlewareFunc {
}
}

// loadSuperAdminPlan reads an org's plan for a super admin, who can open any org without being a member.
// isSuperAdmin is false (and plan empty) for everyone else; an org with no billing row yields an empty plan.
func (am *AuthMiddleware) loadSuperAdminPlan(ctx context.Context, userID, orgID int64) (plan sql.NullString, isSuperAdmin bool, err error) {
err = am.db.QueryRowContext(ctx, `
SELECT EXISTS(
SELECT 1 FROM user_roles ur
JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = $1 AND r.name = $2
)
`, userID, RoleSuperAdmin).Scan(&isSuperAdmin)
if err != nil || !isSuperAdmin {
return plan, false, err
}
err = am.db.QueryRowContext(ctx,
`SELECT current_plan_code FROM org_billing_state WHERE org_id = $1`, orgID).Scan(&plan)
if err == sql.ErrNoRows {
err = nil
}
return plan, true, err
}

// BuildOrgContextFromHeader middleware extracts org_id from X-Org-Context header and validates org exists
func (am *AuthMiddleware) BuildOrgContextFromHeader() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
Expand Down
Loading
Loading