mirror of
https://github.com/juanfont/headscale.git
synced 2026-08-07 07:48:44 +09:00
hscontrol/api/v1: build responses from state views, not proto
Convert directly from NodeView/UserView/PreAuthKeyView (no AsStruct copies, no proto bridge), preserving the view types on the read path. Add UserView.Username and drop the two state.go uses of PreAuthKey.Proto().
This commit is contained in:
@@ -40,7 +40,7 @@ func (s *Server) ListApiKeys(_ context.Context) (*oas.ListApiKeysOK, error) {
|
||||
|
||||
out := make([]oas.ApiKey, len(keys))
|
||||
for i := range keys {
|
||||
out[i] = oasAPIKey(keys[i].Proto())
|
||||
out[i] = oasAPIKey(&keys[i])
|
||||
}
|
||||
|
||||
return &oas.ListApiKeysOK{ApiKeys: out}, nil
|
||||
|
||||
+164
-86
@@ -1,24 +1,24 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// This file bridges the existing proto response builders (the Proto() methods
|
||||
// on the state types) to the ogen API types. Reusing Proto() guarantees the
|
||||
// new HTTP API surfaces exactly the same data the gRPC/gateway stack did
|
||||
// (username fallback, masked key prefixes, online computation, the
|
||||
// TaggedDevices substitution, …) without reimplementing it.
|
||||
// This file converts the state-layer types into the ogen API types. It reads
|
||||
// the copy-on-write view types (NodeView, UserView, PreAuthKeyView) directly so
|
||||
// no node/user is deep-copied on the read path, and reproduces exactly what the
|
||||
// previous proto builders emitted — username fallback, masked key prefixes,
|
||||
// online computation, the register-method enum.
|
||||
//
|
||||
// Unlike grpc-gateway (which marshalled with EmitUnpopulated), these converters
|
||||
// omit zero-value and absent fields — empty strings, false booleans, zero
|
||||
// numbers, empty arrays, nil timestamps/objects, and the unspecified register
|
||||
// method. See docs/v1-ogen/CHANGES.md. When the proto stack is removed, these
|
||||
// converters are rewritten to read the state types directly.
|
||||
//
|
||||
// Converters are added here as each resource group is migrated.
|
||||
// method. See docs/v1-ogen/CHANGES.md.
|
||||
|
||||
func optString(s string) oas.OptString {
|
||||
if s == "" {
|
||||
@@ -36,14 +36,6 @@ func optUint64(v uint64) oas.OptUint64 {
|
||||
return oas.NewOptUint64(v)
|
||||
}
|
||||
|
||||
func optTime(ts *timestamppb.Timestamp) oas.OptDateTime {
|
||||
if ts == nil {
|
||||
return oas.OptDateTime{}
|
||||
}
|
||||
|
||||
return oas.NewOptDateTime(ts.AsTime())
|
||||
}
|
||||
|
||||
func optBool(b bool) oas.OptBool {
|
||||
if !b {
|
||||
return oas.OptBool{}
|
||||
@@ -52,6 +44,31 @@ func optBool(b bool) oas.OptBool {
|
||||
return oas.NewOptBool(b)
|
||||
}
|
||||
|
||||
// optTimeVal sets a timestamp from a value; the previous proto builders always
|
||||
// emitted these fields (timestamppb.New is never nil), so they are always set.
|
||||
func optTimeVal(t time.Time) oas.OptDateTime {
|
||||
return oas.NewOptDateTime(t)
|
||||
}
|
||||
|
||||
// optTimePtr sets a timestamp only when present, matching the proto builders'
|
||||
// nil checks.
|
||||
func optTimePtr(t *time.Time) oas.OptDateTime {
|
||||
if t == nil {
|
||||
return oas.OptDateTime{}
|
||||
}
|
||||
|
||||
return oas.NewOptDateTime(*t)
|
||||
}
|
||||
|
||||
// optTimeVP sets a timestamp from a view's optional pointer.
|
||||
func optTimeVP(p views.ValuePointer[time.Time]) oas.OptDateTime {
|
||||
if !p.Valid() {
|
||||
return oas.OptDateTime{}
|
||||
}
|
||||
|
||||
return oas.NewOptDateTime(p.Get())
|
||||
}
|
||||
|
||||
// strs normalises an empty slice to nil so it is omitted from the response
|
||||
// rather than emitted as an empty array.
|
||||
func strs(s []string) []string {
|
||||
@@ -62,86 +79,147 @@ func strs(s []string) []string {
|
||||
return s
|
||||
}
|
||||
|
||||
func optUser(u *v1.User) oas.OptUser {
|
||||
if u == nil {
|
||||
func oasRegisterMethod(rm string) oas.OptRegisterMethod {
|
||||
var v oas.RegisterMethod
|
||||
|
||||
switch rm {
|
||||
case "authkey":
|
||||
v = "REGISTER_METHOD_AUTH_KEY"
|
||||
case "oidc":
|
||||
v = "REGISTER_METHOD_OIDC"
|
||||
case "cli":
|
||||
v = "REGISTER_METHOD_CLI"
|
||||
default:
|
||||
return oas.OptRegisterMethod{}
|
||||
}
|
||||
|
||||
return oas.NewOptRegisterMethod(v)
|
||||
}
|
||||
|
||||
func oasUser(u types.UserView) oas.User {
|
||||
// Use Name if set, otherwise the display-friendly Username() fallback.
|
||||
name := u.Name()
|
||||
if name == "" {
|
||||
name = u.Username()
|
||||
}
|
||||
|
||||
return oas.User{
|
||||
ID: optUint64(uint64(u.Model().ID)),
|
||||
Name: optString(name),
|
||||
CreatedAt: optTimeVal(u.Model().CreatedAt),
|
||||
DisplayName: optString(u.DisplayName()),
|
||||
Email: optString(u.Email()),
|
||||
ProviderId: optString(u.ProviderIdentifier().String),
|
||||
Provider: optString(u.Provider()),
|
||||
ProfilePicUrl: optString(u.ProfilePicURL()),
|
||||
}
|
||||
}
|
||||
|
||||
func optUser(u types.UserView) oas.OptUser {
|
||||
if !u.Valid() {
|
||||
return oas.OptUser{}
|
||||
}
|
||||
|
||||
return oas.NewOptUser(oasUser(u))
|
||||
}
|
||||
|
||||
func oasPreAuthKey(k *v1.PreAuthKey) oas.PreAuthKey {
|
||||
return oas.PreAuthKey{
|
||||
User: optUser(k.GetUser()),
|
||||
ID: optUint64(k.GetId()),
|
||||
Key: optString(k.GetKey()),
|
||||
Reusable: optBool(k.GetReusable()),
|
||||
Ephemeral: optBool(k.GetEphemeral()),
|
||||
Used: optBool(k.GetUsed()),
|
||||
Expiration: optTime(k.GetExpiration()),
|
||||
CreatedAt: optTime(k.GetCreatedAt()),
|
||||
AclTags: strs(k.GetAclTags()),
|
||||
}
|
||||
}
|
||||
|
||||
func optPreAuthKey(k *v1.PreAuthKey) oas.OptPreAuthKey {
|
||||
if k == nil {
|
||||
return oas.OptPreAuthKey{}
|
||||
func apiKeyMaskedPrefix(prefix string) string {
|
||||
if len(prefix) == types.NewAPIKeyPrefixLength {
|
||||
return "hskey-api-" + prefix + "-***"
|
||||
}
|
||||
|
||||
return oas.NewOptPreAuthKey(oasPreAuthKey(k))
|
||||
return prefix + "***"
|
||||
}
|
||||
|
||||
func optRegisterMethod(rm v1.RegisterMethod) oas.OptRegisterMethod {
|
||||
if rm == v1.RegisterMethod_REGISTER_METHOD_UNSPECIFIED {
|
||||
return oas.OptRegisterMethod{}
|
||||
}
|
||||
|
||||
return oas.NewOptRegisterMethod(oas.RegisterMethod(rm.String()))
|
||||
}
|
||||
|
||||
func oasNode(n *v1.Node) oas.Node {
|
||||
return oas.Node{
|
||||
ID: optUint64(n.GetId()),
|
||||
MachineKey: optString(n.GetMachineKey()),
|
||||
NodeKey: optString(n.GetNodeKey()),
|
||||
DiscoKey: optString(n.GetDiscoKey()),
|
||||
IpAddresses: strs(n.GetIpAddresses()),
|
||||
Name: optString(n.GetName()),
|
||||
User: optUser(n.GetUser()),
|
||||
LastSeen: optTime(n.GetLastSeen()),
|
||||
Expiry: optTime(n.GetExpiry()),
|
||||
PreAuthKey: optPreAuthKey(n.GetPreAuthKey()),
|
||||
CreatedAt: optTime(n.GetCreatedAt()),
|
||||
RegisterMethod: optRegisterMethod(n.GetRegisterMethod()),
|
||||
GivenName: optString(n.GetGivenName()),
|
||||
Online: optBool(n.GetOnline()),
|
||||
ApprovedRoutes: strs(n.GetApprovedRoutes()),
|
||||
AvailableRoutes: strs(n.GetAvailableRoutes()),
|
||||
SubnetRoutes: strs(n.GetSubnetRoutes()),
|
||||
Tags: strs(n.GetTags()),
|
||||
}
|
||||
}
|
||||
|
||||
func oasAPIKey(k *v1.ApiKey) oas.ApiKey {
|
||||
func oasAPIKey(k *types.APIKey) oas.ApiKey {
|
||||
return oas.ApiKey{
|
||||
ID: optUint64(k.GetId()),
|
||||
Prefix: optString(k.GetPrefix()),
|
||||
Expiration: optTime(k.GetExpiration()),
|
||||
CreatedAt: optTime(k.GetCreatedAt()),
|
||||
LastSeen: optTime(k.GetLastSeen()),
|
||||
ID: optUint64(k.ID),
|
||||
Prefix: optString(apiKeyMaskedPrefix(k.Prefix)),
|
||||
Expiration: optTimePtr(k.Expiration),
|
||||
CreatedAt: optTimePtr(k.CreatedAt),
|
||||
LastSeen: optTimePtr(k.LastSeen),
|
||||
}
|
||||
}
|
||||
|
||||
func oasUser(u *v1.User) oas.User {
|
||||
return oas.User{
|
||||
ID: optUint64(u.GetId()),
|
||||
Name: optString(u.GetName()),
|
||||
CreatedAt: optTime(u.GetCreatedAt()),
|
||||
DisplayName: optString(u.GetDisplayName()),
|
||||
Email: optString(u.GetEmail()),
|
||||
ProviderId: optString(u.GetProviderId()),
|
||||
Provider: optString(u.GetProvider()),
|
||||
ProfilePicUrl: optString(u.GetProfilePicUrl()),
|
||||
func preAuthKeyMaskedPrefix(prefix string) string {
|
||||
if prefix != "" {
|
||||
return "hskey-auth-" + prefix + "-***"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func oasPreAuthKey(k types.PreAuthKeyView) oas.PreAuthKey {
|
||||
out := oas.PreAuthKey{
|
||||
User: optUser(k.User()),
|
||||
ID: optUint64(k.ID()),
|
||||
Reusable: optBool(k.Reusable()),
|
||||
Ephemeral: optBool(k.Ephemeral()),
|
||||
Used: optBool(k.Used()),
|
||||
Expiration: optTimeVP(k.Expiration()),
|
||||
CreatedAt: optTimeVP(k.CreatedAt()),
|
||||
AclTags: strs(k.Tags().AsSlice()),
|
||||
}
|
||||
|
||||
// New keys show the masked prefix; legacy keys (with a plaintext key) show
|
||||
// the full key for backwards compatibility.
|
||||
if masked := preAuthKeyMaskedPrefix(k.Prefix()); masked != "" {
|
||||
out.Key = optString(masked)
|
||||
} else if k.Key() != "" {
|
||||
out.Key = optString(k.Key())
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// oasPreAuthKeyNew converts a freshly created pre-auth key, which exposes the
|
||||
// full secret key exactly once and has no view type.
|
||||
func oasPreAuthKeyNew(k *types.PreAuthKeyNew) oas.PreAuthKey {
|
||||
out := oas.PreAuthKey{
|
||||
ID: optUint64(k.ID),
|
||||
Key: optString(k.Key),
|
||||
Reusable: optBool(k.Reusable),
|
||||
Ephemeral: optBool(k.Ephemeral),
|
||||
Expiration: optTimePtr(k.Expiration),
|
||||
CreatedAt: optTimePtr(k.CreatedAt),
|
||||
AclTags: strs(k.Tags),
|
||||
}
|
||||
|
||||
if k.User != nil {
|
||||
out.User = oas.NewOptUser(oasUser(k.User.View()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// oasNode converts a node view. user is the user to present (the node's own
|
||||
// user for most callers, or TaggedDevices for tagged nodes); subnetRoutes
|
||||
// carries the routes actively served from the node (primary + exit), which only
|
||||
// some callers populate.
|
||||
func oasNode(nv types.NodeView, user types.UserView, subnetRoutes []string) oas.Node {
|
||||
out := oas.Node{
|
||||
ID: optUint64(uint64(nv.ID())),
|
||||
MachineKey: optString(nv.MachineKey().String()),
|
||||
NodeKey: optString(nv.NodeKey().String()),
|
||||
DiscoKey: optString(nv.DiscoKey().String()),
|
||||
IpAddresses: strs(nv.IPsAsString()),
|
||||
Name: optString(nv.Hostname()),
|
||||
User: optUser(user),
|
||||
CreatedAt: optTimeVal(nv.CreatedAt()),
|
||||
RegisterMethod: oasRegisterMethod(nv.RegisterMethod()),
|
||||
GivenName: optString(nv.GivenName()),
|
||||
Online: optBool(nv.IsOnline().GetOr(false)),
|
||||
ApprovedRoutes: strs(util.PrefixesToString(nv.ApprovedRoutes().AsSlice())),
|
||||
AvailableRoutes: strs(util.PrefixesToString(nv.AnnouncedRoutes())),
|
||||
SubnetRoutes: strs(subnetRoutes),
|
||||
Tags: strs(nv.Tags().AsSlice()),
|
||||
LastSeen: optTimeVP(nv.LastSeen()),
|
||||
Expiry: optTimeVP(nv.Expiry()),
|
||||
}
|
||||
|
||||
if nv.AuthKey().Valid() {
|
||||
out.PreAuthKey = oas.NewOptPreAuthKey(oasPreAuthKey(nv.AuthKey()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
+15
-14
@@ -49,7 +49,7 @@ func (s *Server) RegisterNode(
|
||||
|
||||
s.change(nodeChange, routeChange)
|
||||
|
||||
return &oas.RegisterNodeOK{Node: oas.NewOptNode(oasNode(node.Proto()))}, nil
|
||||
return &oas.RegisterNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// GetNode returns a node by id.
|
||||
@@ -59,7 +59,7 @@ func (s *Server) GetNode(_ context.Context, params oas.GetNodeParams) (*oas.GetN
|
||||
return nil, notFound("node not found")
|
||||
}
|
||||
|
||||
return &oas.GetNodeOK{Node: oas.NewOptNode(oasNode(node.Proto()))}, nil
|
||||
return &oas.GetNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// SetTags sets the ACL tags of a node, converting it to a tagged node.
|
||||
@@ -93,7 +93,7 @@ func (s *Server) SetTags(
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
return &oas.SetTagsOK{Node: oas.NewOptNode(oasNode(node.Proto()))}, nil
|
||||
return &oas.SetTagsOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// SetApprovedRoutes sets the approved subnet routes of a node, expanding exit
|
||||
@@ -129,11 +129,12 @@ func (s *Server) SetApprovedRoutes(
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
proto := node.Proto()
|
||||
// SubnetRoutes carries only the routes actively served from the node.
|
||||
proto.SubnetRoutes = util.PrefixesToString(s.state.GetNodePrimaryRoutes(node.ID()))
|
||||
subnetRoutes := util.PrefixesToString(s.state.GetNodePrimaryRoutes(node.ID()))
|
||||
|
||||
return &oas.SetApprovedRoutesOK{Node: oas.NewOptNode(oasNode(proto))}, nil
|
||||
return &oas.SetApprovedRoutesOK{
|
||||
Node: oas.NewOptNode(oasNode(node, node.User(), subnetRoutes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteNode deletes a node.
|
||||
@@ -181,7 +182,7 @@ func (s *Server) ExpireNode(
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
return &oas.ExpireNodeOK{Node: oas.NewOptNode(oasNode(node.Proto()))}, nil
|
||||
return &oas.ExpireNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// RenameNode renames a node.
|
||||
@@ -196,7 +197,7 @@ func (s *Server) RenameNode(
|
||||
|
||||
s.change(nodeChange)
|
||||
|
||||
return &oas.RenameNodeOK{Node: oas.NewOptNode(oasNode(node.Proto()))}, nil
|
||||
return &oas.RenameNodeOK{Node: oas.NewOptNode(oasNode(node, node.User(), nil))}, nil
|
||||
}
|
||||
|
||||
// ListNodes lists nodes, optionally filtered by user, sorted by id.
|
||||
@@ -227,17 +228,17 @@ func (s *Server) nodesToOAS(nodes views.Slice[types.NodeView]) []oas.Node {
|
||||
out := make([]oas.Node, nodes.Len())
|
||||
|
||||
for index, node := range nodes.All() {
|
||||
proto := node.Proto()
|
||||
|
||||
// Tagged nodes are presented as the TaggedDevices user.
|
||||
user := node.User()
|
||||
if node.IsTagged() {
|
||||
proto.User = types.TaggedDevices.Proto()
|
||||
user = types.TaggedDevices.View()
|
||||
}
|
||||
|
||||
proto.SubnetRoutes = util.PrefixesToString(
|
||||
subnetRoutes := util.PrefixesToString(
|
||||
append(s.state.GetNodePrimaryRoutes(node.ID()), node.ExitRoutes()...),
|
||||
)
|
||||
|
||||
out[index] = oasNode(proto)
|
||||
out[index] = oasNode(node, user, subnetRoutes)
|
||||
}
|
||||
|
||||
slices.SortFunc(out, func(a, b oas.Node) int { return cmp.Compare(a.ID.Or(0), b.ID.Or(0)) })
|
||||
@@ -307,5 +308,5 @@ func (s *Server) DebugCreateNode(
|
||||
},
|
||||
}
|
||||
|
||||
return &oas.DebugCreateNodeOK{Node: oas.NewOptNode(oasNode(echoNode.Proto()))}, nil
|
||||
return &oas.DebugCreateNodeOK{Node: oas.NewOptNode(oasNode(echoNode.View(), echoNode.View().User(), nil))}, nil
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *Server) CreatePreAuthKey(
|
||||
}
|
||||
|
||||
return &oas.CreatePreAuthKeyOK{
|
||||
PreAuthKey: oas.NewOptPreAuthKey(oasPreAuthKey(preAuthKey.Proto())),
|
||||
PreAuthKey: oas.NewOptPreAuthKey(oasPreAuthKeyNew(preAuthKey)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (s *Server) ListPreAuthKeys(_ context.Context) (*oas.ListPreAuthKeysOK, err
|
||||
|
||||
out := make([]oas.PreAuthKey, len(keys))
|
||||
for i := range keys {
|
||||
out[i] = oasPreAuthKey(keys[i].Proto())
|
||||
out[i] = oasPreAuthKey(keys[i].View())
|
||||
}
|
||||
|
||||
return &oas.ListPreAuthKeysOK{PreAuthKeys: out}, nil
|
||||
|
||||
@@ -29,7 +29,7 @@ func (s *Server) CreateUser(
|
||||
|
||||
s.change(policyChanged)
|
||||
|
||||
return &oas.CreateUserOK{User: oas.NewOptUser(oasUser(user.Proto()))}, nil
|
||||
return &oas.CreateUserOK{User: oas.NewOptUser(oasUser(user.View()))}, nil
|
||||
}
|
||||
|
||||
// ListUsers lists users, optionally filtered by id, name, or email, sorted by id.
|
||||
@@ -63,7 +63,7 @@ func (s *Server) ListUsers(
|
||||
|
||||
out := make([]oas.User, len(users))
|
||||
for i := range users {
|
||||
out[i] = oasUser(users[i].Proto())
|
||||
out[i] = oasUser(users[i].View())
|
||||
}
|
||||
|
||||
return &oas.ListUsersOK{Users: out}, nil
|
||||
@@ -91,7 +91,7 @@ func (s *Server) RenameUser(
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return &oas.RenameUserOK{User: oas.NewOptUser(oasUser(newUser.Proto()))}, nil
|
||||
return &oas.RenameUserOK{User: oas.NewOptUser(oasUser(newUser.View()))}, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user and distributes the resulting policy change.
|
||||
|
||||
@@ -1793,7 +1793,7 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro
|
||||
if params.PreAuthKey.IsTagged() {
|
||||
// Tagged nodes are owned by their tags, not a user.
|
||||
// UserID is intentionally left nil.
|
||||
nodeToRegister.Tags = params.PreAuthKey.Proto().GetAclTags()
|
||||
nodeToRegister.Tags = params.PreAuthKey.Tags
|
||||
|
||||
// Tagged nodes have key expiry disabled.
|
||||
nodeToRegister.Expiry = nil
|
||||
@@ -2427,7 +2427,7 @@ func (s *State) HandleNodeFromPreAuthKey(
|
||||
// user-less and never expire). Only update AuthKey reference
|
||||
// otherwise.
|
||||
if pak.IsTagged() && !node.IsTagged() {
|
||||
node.Tags = pak.Proto().GetAclTags()
|
||||
node.Tags = pak.Tags
|
||||
node.UserID = nil
|
||||
node.User = nil
|
||||
node.Expiry = nil
|
||||
|
||||
@@ -129,6 +129,16 @@ func (u *User) Username() string {
|
||||
)
|
||||
}
|
||||
|
||||
// Username returns the display-friendly identifier for the user view,
|
||||
// mirroring [User.Username].
|
||||
func (v UserView) Username() string {
|
||||
if !v.Valid() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return v.ж.Username()
|
||||
}
|
||||
|
||||
// Display returns the [User.DisplayName] if it exists, otherwise
|
||||
// it will return the [User.Username].
|
||||
func (u *User) Display() string {
|
||||
|
||||
Reference in New Issue
Block a user