mirror of
https://github.com/juanfont/headscale.git
synced 2026-08-07 07:48:44 +09:00
29fc14dd73
Serve the API over the unix socket (auth bypassed; socket permissions are the trust boundary) and convert every CLI command from the gRPC client to the generated ogen client. Remote CLI now uses the HTTP API URL.
108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
package apiv1
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
oas "github.com/juanfont/headscale/gen/api/v1"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
)
|
|
|
|
var errAuthRejected = errors.New("auth request rejected")
|
|
|
|
type socketAuthContextKey struct{}
|
|
|
|
// WithSocketAuth marks ctx as originating from the trusted local unix socket,
|
|
// where filesystem permissions are the trust boundary. Requests carrying this
|
|
// marker bypass bearer-token validation. The server mounts this on the
|
|
// socket-only listener.
|
|
func WithSocketAuth(ctx context.Context) context.Context {
|
|
return context.WithValue(ctx, socketAuthContextKey{}, true)
|
|
}
|
|
|
|
func isSocketAuth(ctx context.Context) bool {
|
|
v, _ := ctx.Value(socketAuthContextKey{}).(bool)
|
|
return v
|
|
}
|
|
|
|
// HandleBearerAuth validates the API key bearer token against the state layer.
|
|
// A missing or malformed Authorization header is reported by ogen before this
|
|
// is reached. Any validation failure — a malformed/unknown key (which
|
|
// [state.State.ValidateAPIKey] reports as an error) or an expired/invalid one —
|
|
// is a 401, matching the previous middleware which rejected every such case
|
|
// with Unauthorized.
|
|
func (s *Server) HandleBearerAuth(
|
|
ctx context.Context,
|
|
_ oas.OperationName,
|
|
t oas.BearerAuth,
|
|
) (context.Context, error) {
|
|
// Requests from the local unix socket are trusted via filesystem permissions.
|
|
if isSocketAuth(ctx) {
|
|
return ctx, nil
|
|
}
|
|
|
|
valid, err := s.state.ValidateAPIKey(t.Token)
|
|
if err != nil || !valid {
|
|
return ctx, apiError(http.StatusUnauthorized, "invalid API key")
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|
|
|
|
// AuthRegister registers a node via an auth id; it is an alias of RegisterNode.
|
|
func (s *Server) AuthRegister(
|
|
ctx context.Context,
|
|
req *oas.AuthRegisterReq,
|
|
) (*oas.AuthRegisterOK, error) {
|
|
resp, err := s.RegisterNode(ctx, oas.RegisterNodeParams{
|
|
Key: oas.NewOptString(req.AuthId.Or("")),
|
|
User: oas.NewOptString(req.User.Or("")),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &oas.AuthRegisterOK{Node: resp.Node}, nil
|
|
}
|
|
|
|
// AuthApprove approves a pending auth session.
|
|
func (s *Server) AuthApprove(_ context.Context, req *oas.AuthApproveReq) error {
|
|
authReq, apiErr := s.pendingAuth(req.AuthId.Or(""))
|
|
if apiErr != nil {
|
|
return apiErr
|
|
}
|
|
|
|
authReq.FinishAuth(types.AuthVerdict{})
|
|
|
|
return nil
|
|
}
|
|
|
|
// AuthReject rejects a pending auth session.
|
|
func (s *Server) AuthReject(_ context.Context, req *oas.AuthRejectReq) error {
|
|
authReq, apiErr := s.pendingAuth(req.AuthId.Or(""))
|
|
if apiErr != nil {
|
|
return apiErr
|
|
}
|
|
|
|
authReq.FinishAuth(types.AuthVerdict{Err: errAuthRejected})
|
|
|
|
return nil
|
|
}
|
|
|
|
// pendingAuth resolves an auth id to its cached, in-progress auth request.
|
|
// An unparseable id is a 400; an unknown one is a 404.
|
|
func (s *Server) pendingAuth(authID string) (*types.AuthRequest, *oas.ErrorStatusCode) {
|
|
id, err := types.AuthIDFromString(authID)
|
|
if err != nil {
|
|
return nil, badRequest("invalid auth_id: " + err.Error())
|
|
}
|
|
|
|
authReq, ok := s.state.GetAuthCacheEntry(id)
|
|
if !ok {
|
|
return nil, notFound("no pending auth session for auth_id " + id.String())
|
|
}
|
|
|
|
return authReq, nil
|
|
}
|