mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 16:01:28 +00:00
Merge remote-tracking branch 'origin/main' into feature/mailbox-fair-use-allowance
This commit is contained in:
@@ -171,6 +171,37 @@ func (h *Handler) RevokeAPIKey(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "revoked"})
|
||||
}
|
||||
|
||||
// RevokeOwnAPIKey revokes the key the request was made with.
|
||||
//
|
||||
// Deliberately outside the API_KEYS scope gate: a credential must always be
|
||||
// able to end itself. Requiring a privilege to sign out means a read-only key
|
||||
// on a laptop someone is handing back stays live, which is the opposite of
|
||||
// what a `warmbly auth logout` promises.
|
||||
func (h *Handler) RevokeOwnAPIKey(c *gin.Context) {
|
||||
keyID := middleware.GetAPIKeyID(c)
|
||||
if keyID == nil {
|
||||
errx.JSON(c, errx.New(errx.BadRequest, "this endpoint revokes the API key it is called with, and this request did not use one"))
|
||||
return
|
||||
}
|
||||
orgID := middleware.GetOrganizationID(c)
|
||||
if orgID == nil {
|
||||
errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
|
||||
return
|
||||
}
|
||||
|
||||
reason := c.Query("reason")
|
||||
if reason == "" {
|
||||
reason = "Revoked by the credential itself"
|
||||
}
|
||||
if xerr := h.APIKeyService.Revoke(c.Request.Context(), *orgID, *keyID, reason); xerr != nil {
|
||||
errx.JSON(c, xerr)
|
||||
return
|
||||
}
|
||||
|
||||
h.auditOrg(c, models.AuditActionRevoke, models.AuditEntityAPIKey, keyID, nil, map[string]string{"self": "true"})
|
||||
c.JSON(http.StatusOK, gin.H{"status": "revoked"})
|
||||
}
|
||||
|
||||
// ListAPIPermissions lists all available API permissions
|
||||
// GET /api-keys/permissions
|
||||
func (h *Handler) ListAPIPermissions(c *gin.Context) {
|
||||
|
||||
@@ -72,6 +72,18 @@ type DeploymentAuthConfig struct {
|
||||
// DocsURL is where to send someone whose signup was refused by deployment
|
||||
// policy rather than by anything they did wrong.
|
||||
DocsURL string `json:"docs_url"`
|
||||
|
||||
// WebsocketURL is the realtime gateway. Served here because a developer
|
||||
// client (the CLI's event stream, an SDK) has no other way to find the
|
||||
// socket on a self-hosted instance. Empty when the instance runs no
|
||||
// realtime service.
|
||||
WebsocketURL string `json:"websocket_url,omitempty"`
|
||||
|
||||
// AppURL is the dashboard origin, the same one every emailed link is built
|
||||
// from. A client that wants to send someone to a page (the CLI's `browse`,
|
||||
// a chat integration) cannot derive it: on a self-hosted instance the host
|
||||
// layout is whatever the operator chose.
|
||||
AppURL string `json:"app_url,omitempty"`
|
||||
}
|
||||
|
||||
// accountsDocsURL is the page every registration refusal points at.
|
||||
@@ -104,5 +116,7 @@ func (h *Handler) AuthConfig(c *gin.Context) {
|
||||
SetupRequired: h.BootstrapService != nil && h.BootstrapService.Required(c.Request.Context()),
|
||||
InvitesRequired: registration == config.RegistrationInviteOnly,
|
||||
DocsURL: accountsDocsURL,
|
||||
WebsocketURL: config.WebsocketURL(),
|
||||
AppURL: config.AppBaseURL(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/warmbly/warmbly/internal/api/middleware"
|
||||
"github.com/warmbly/warmbly/internal/errx"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
)
|
||||
|
||||
// Device-code sign-in for the `warmbly` CLI. The public half (start, poll) is
|
||||
// what the CLI calls; the session half is the browser approval screen.
|
||||
|
||||
func (h *Handler) cliAuthReady(c *gin.Context) bool {
|
||||
if h.CLIAuthService == nil {
|
||||
errx.JSON(c, errx.New(errx.NotImplemented, "CLI sign-in is not enabled on this instance"))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CLIAuthStart opens a handshake for a CLI that holds no key yet.
|
||||
func (h *Handler) CLIAuthStart(c *gin.Context) {
|
||||
if !h.cliAuthReady(c) {
|
||||
return
|
||||
}
|
||||
var req models.CLIAuthStartRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
|
||||
return
|
||||
}
|
||||
res, xerr := h.CLIAuthService.StartCode(c.Request.Context(), req)
|
||||
if xerr != nil {
|
||||
errx.JSON(c, xerr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, res)
|
||||
}
|
||||
|
||||
// CLIAuthPoll is polled by the CLI until a member approves the code. The key is
|
||||
// handed out exactly once, on the poll that follows approval.
|
||||
func (h *Handler) CLIAuthPoll(c *gin.Context) {
|
||||
if !h.cliAuthReady(c) {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
DeviceCode string `json:"device_code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.DeviceCode == "" {
|
||||
errx.JSON(c, errx.New(errx.BadRequest, "device_code is required"))
|
||||
return
|
||||
}
|
||||
res, xerr := h.CLIAuthService.PollCode(c.Request.Context(), req.DeviceCode)
|
||||
if xerr != nil {
|
||||
errx.JSON(c, xerr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
// CLIAuthDescribeCode shows the approving member what they are authorizing.
|
||||
func (h *Handler) CLIAuthDescribeCode(c *gin.Context) {
|
||||
if !h.cliAuthReady(c) {
|
||||
return
|
||||
}
|
||||
code, xerr := h.CLIAuthService.DescribeCode(c.Request.Context(), c.Param("code"))
|
||||
if xerr != nil {
|
||||
errx.JSON(c, xerr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, code)
|
||||
}
|
||||
|
||||
// CLIAuthApproveCode mints the key into the workspace named in the body, not
|
||||
// the session's, because a member with several workspaces picks on the screen.
|
||||
func (h *Handler) CLIAuthApproveCode(c *gin.Context) {
|
||||
if !h.cliAuthReady(c) {
|
||||
return
|
||||
}
|
||||
userID, err := middleware.GetUserUUID(c)
|
||||
if err != nil {
|
||||
errx.JSON(c, errx.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
var req models.CLIAuthApproveRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
|
||||
return
|
||||
}
|
||||
orgID, perr := uuid.Parse(req.OrganizationID)
|
||||
if perr != nil {
|
||||
if sessionOrg := middleware.GetOrganizationID(c); sessionOrg != nil {
|
||||
orgID = *sessionOrg
|
||||
} else {
|
||||
errx.JSON(c, errx.ErrNoOrganization)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
code, xerr := h.CLIAuthService.ApproveCode(c.Request.Context(), c.Param("code"), orgID, userID)
|
||||
if xerr != nil {
|
||||
errx.JSON(c, xerr)
|
||||
return
|
||||
}
|
||||
// Logged against the org that was picked on screen, which is not always the
|
||||
// session's, so this cannot go through auditOrg.
|
||||
h.AuditService.LogAction(c.Request.Context(), orgID, userID, models.AuditActionCreate, models.AuditEntityAPIKey, code.APIKeyID,
|
||||
c.ClientIP(), c.Request.UserAgent(), nil, map[string]string{"source": "cli", "client": code.ClientName, "hostname": code.Hostname})
|
||||
c.JSON(http.StatusOK, code)
|
||||
}
|
||||
|
||||
// CLIAuthDenyCode declines the request. Deliberately not audited: nothing was
|
||||
// created, and a denial is not a change to the workspace.
|
||||
func (h *Handler) CLIAuthDenyCode(c *gin.Context) {
|
||||
if !h.cliAuthReady(c) {
|
||||
return
|
||||
}
|
||||
if xerr := h.CLIAuthService.DenyCode(c.Request.Context(), c.Param("code")); xerr != nil {
|
||||
errx.JSON(c, xerr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/warmbly/warmbly/internal/app/behavior"
|
||||
"github.com/warmbly/warmbly/internal/app/bootstrap"
|
||||
"github.com/warmbly/warmbly/internal/app/campaign"
|
||||
"github.com/warmbly/warmbly/internal/app/cliauth"
|
||||
"github.com/warmbly/warmbly/internal/app/cloudlink"
|
||||
"github.com/warmbly/warmbly/internal/app/compose"
|
||||
"github.com/warmbly/warmbly/internal/app/contact"
|
||||
@@ -312,6 +313,9 @@ type Handler struct {
|
||||
PoolLinkService poollink.Service
|
||||
CloudLinkService cloudlink.Service
|
||||
|
||||
// Device-code sign-in for the `warmbly` CLI. Nil-safe: routes answer 501.
|
||||
CLIAuthService cliauth.Service
|
||||
|
||||
// Infrastructure liveness probes for the admin System Status page.
|
||||
// Wired in cmd/backend/main.go where the concrete clients live.
|
||||
SystemChecker *sysstatus.Checker
|
||||
|
||||
Reference in New Issue
Block a user