This commit is contained in:
2026-07-28 21:01:58 -05:00
parent 510d5490f9
commit f56ac6a2c4
7 changed files with 219 additions and 13 deletions
+74 -11
View File
@@ -77,8 +77,12 @@ func (s *Server) handleExchange(w http.ResponseWriter, r *http.Request) {
status = http.StatusForbidden
message = "authorization is invalid, expired, revoked, or at its host limit"
}
_ = s.audit(r.Context(), "code_exchange_failed", "", nil,
request.Hostname, "", sourceIP, err.Error())
var failedAuthorizationID *uint64
if authorizationID != 0 {
failedAuthorizationID = &authorizationID
}
_ = s.audit(r.Context(), "code_exchange_failed", "", failedAuthorizationID,
request.Hostname, "", sourceIP, codeExchangeFailureDetails(request.Code, err))
writeJSON(w, status, map[string]string{"error": message})
return
}
@@ -110,6 +114,55 @@ func (s *Server) exchangeRateLimited(r *http.Request, sourceIP string) (bool, er
return attempts >= 10, err
}
func codeExchangeFailureDetails(code string, err error) string {
reason := "internal_error"
switch {
case errors.Is(err, errCodeNotFound):
reason = "code_not_found"
case errors.Is(err, errCodeExpired):
reason = "code_expired"
case errors.Is(err, errCodeRevoked):
reason = "code_revoked"
case errors.Is(err, errActionNotAuthorized):
reason = "action_not_authorized"
case errors.Is(err, errPackageNotAuthorized):
reason = "package_not_authorized"
case errors.Is(err, errHostLimitReached):
reason = "host_limit_reached"
case errors.Is(err, errAuthorizationDenied):
reason = "authorization_denied"
}
format := "unexpected"
if validDeploymentCodeFormat(code) {
format = "expected"
}
hint := "unavailable"
if strings.HasPrefix(code, "TAPM-") && len(code) >= 5 {
hint = code[len(code)-5:]
}
return fmt.Sprintf(
"reason=%s code_hint=%s normalized_length=%d format=%s",
reason, hint, len(code), format,
)
}
func validDeploymentCodeFormat(code string) bool {
if len(code) != 16 || !strings.HasPrefix(code, "TAPM-") || code[10] != '-' {
return false
}
const allowed = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
for index, character := range code {
if index < 5 || index == 10 {
continue
}
if !strings.ContainsRune(allowed, character) {
return false
}
}
return true
}
func (s *Server) exchangeDeploymentCode(
r *http.Request,
request exchangeRequest,
@@ -127,21 +180,31 @@ func (s *Server) exchangeDeploymentCode(
var authorizationID uint64
var hostLimit int
var expiresAt time.Time
var authorizationStatus string
err = tx.QueryRowContext(
r.Context(),
`SELECT id, host_limit, expires_at
`SELECT id, host_limit, expires_at,
CASE
WHEN revoked_at IS NOT NULL THEN 'revoked'
WHEN expires_at <= CURRENT_TIMESTAMP THEN 'expired'
ELSE 'active'
END
FROM authorizations
WHERE code_hash = ?
AND revoked_at IS NULL
AND expires_at > CURRENT_TIMESTAMP`,
WHERE code_hash = ?`,
codeHash[:],
).Scan(&authorizationID, &hostLimit, &expiresAt)
).Scan(&authorizationID, &hostLimit, &expiresAt, &authorizationStatus)
if errors.Is(err, sql.ErrNoRows) {
return response, 0, errAuthorizationDenied
return response, 0, errCodeNotFound
}
if err != nil {
return response, 0, err
}
switch authorizationStatus {
case "revoked":
return response, authorizationID, errCodeRevoked
case "expired":
return response, authorizationID, errCodeExpired
}
if request.RequestedAction != "" {
var authorized int
@@ -158,7 +221,7 @@ func (s *Server) exchangeDeploymentCode(
return response, 0, err
}
if authorized != 1 {
return response, 0, errAuthorizationDenied
return response, authorizationID, errActionNotAuthorized
}
}
if request.RequestedPackage != "" {
@@ -176,7 +239,7 @@ func (s *Server) exchangeDeploymentCode(
return response, 0, err
}
if authorized != 1 {
return response, 0, errAuthorizationDenied
return response, authorizationID, errPackageNotAuthorized
}
}
@@ -197,7 +260,7 @@ func (s *Server) exchangeDeploymentCode(
return response, 0, err
}
if hostCount >= hostLimit {
return response, 0, errAuthorizationDenied
return response, authorizationID, errHostLimitReached
}
result, err := tx.ExecContext(
r.Context(),