mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 20:49:24 +09:00
api/v2: name the join key in a device's oauthClientId
For a key a token made, Tailscale names the token's client, which is not recorded, so the field is left out.
This commit is contained in:
1 parent
8ea774564d
commit
73ac0bc010
3 files changed
+21
-17
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
@@ -40,6 +41,10 @@ type Device struct {
|
||||
OS string `json:"os"`
|
||||
ClientVersion string `json:"clientVersion"`
|
||||
UpdateAvailable bool `json:"updateAvailable"`
|
||||
// OAuthClientID names the auth key the device joined with. For a key a
|
||||
// token made, Tailscale names the token's OAuth client, which headscale
|
||||
// does not record, so it is left out.
|
||||
OAuthClientID string `json:"oauthClientId,omitempty"`
|
||||
|
||||
// Populated only when fields=all is requested.
|
||||
AdvertisedRoutes []string `json:"advertisedRoutes,omitempty"`
|
||||
@@ -456,6 +461,10 @@ func deviceFromView(view types.NodeView, allFields bool, baseDomain string) Devi
|
||||
ClientVersion: hostinfoVersion(view),
|
||||
}
|
||||
|
||||
if key := view.AuthKey(); key.Valid() && !key.CreatedByToken() {
|
||||
d.OAuthClientID = strconv.FormatUint(key.ID(), util.Base10)
|
||||
}
|
||||
|
||||
if view.Expiry().Valid() {
|
||||
exp := view.Expiry().Get()
|
||||
d.Expires = &exp
|
||||
|
||||
@@ -96,20 +96,8 @@ var apiv2CompatSkipReasons = map[string]string{
|
||||
"oauth/B7/0/message": deletedClientCredentialText,
|
||||
"oauth/B7/1/message": deletedClientCredentialText,
|
||||
|
||||
"oauth/device:before/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:ci-runner/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:joiner/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:laptop/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:managed/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:mixed/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:prod-db/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:router/all/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:ci-runner/default/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:laptop/default/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:managed/default/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:mixed/default/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:prod-db/default/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:router/default/device-oauth-client": deviceOAuthClientID,
|
||||
"oauth/device:before/all/device-oauth-client": deviceOAuthClientMaker,
|
||||
"oauth/device:joiner/all/device-oauth-client": deviceOAuthClientMaker,
|
||||
|
||||
"oauth/device:before/empty-route-lists": emptyRouteListsOmitted,
|
||||
"oauth/device:ci-runner/empty-route-lists": emptyRouteListsOmitted,
|
||||
@@ -178,7 +166,6 @@ const (
|
||||
deletedClientGone = "DELETED_CLIENT_GONE: SaaS keeps a deleted OAuth client readable as invalid and revoked; headscale removes it"
|
||||
deletedClientCredentialText = "DELETED_CLIENT_CREDENTIAL_TEXT: SaaS refuses a deleted client's secret and tokens with API token invalid; headscale says invalid client credentials and unauthorized"
|
||||
missingScopeText = "MISSING_SCOPE_TEXT: SaaS refuses a delete outside the token's scopes with calling actor does not have enough permissions; headscale names the missing scope"
|
||||
deviceOAuthClientID = "DEVICE_OAUTH_CLIENT_ID: SaaS device bodies carry oauthClientId, naming the join key or the OAuth client that made it; headscale's have no such field"
|
||||
emptyRouteListsOmitted = "EMPTY_ROUTE_LISTS_OMITTED: SaaS sends advertisedRoutes and enabledRoutes as [] for a device without routes under fields=all; headscale leaves both out"
|
||||
noV2Validate = "NO_V2_VALIDATE: headscale's v2 API has no /acl/validate"
|
||||
hujsonFormatted = "HUJSON_FORMATTED: SaaS stores a HuJSON policy formatted, comments kept; headscale stores the bytes as sent"
|
||||
@@ -186,8 +173,9 @@ const (
|
||||
|
||||
// SaaS behaviour recorded as it is; following it is the maintainer's call.
|
||||
const (
|
||||
missingScopeNotFound = "MISSING_SCOPE_NOT_FOUND: SaaS answers a call outside the token's scopes with 404 not found; headscale answers 403"
|
||||
unknownScopeGranted = "UNKNOWN_SCOPE_GRANTED: SaaS ignores an unknown requested scope and grants the client's own; headscale grants the unknown scope alone"
|
||||
deviceOAuthClientMaker = "DEVICE_OAUTH_CLIENT_ID: SaaS names, in oauthClientId, the OAuth client whose token made the device's key; headscale does not record which client that was, so it leaves the field out"
|
||||
missingScopeNotFound = "MISSING_SCOPE_NOT_FOUND: SaaS answers a call outside the token's scopes with 404 not found; headscale answers 403"
|
||||
unknownScopeGranted = "UNKNOWN_SCOPE_GRANTED: SaaS ignores an unknown requested scope and grants the client's own; headscale grants the unknown scope alone"
|
||||
)
|
||||
|
||||
// TestAPIv2OAuthCompat replays OAuth clients, token exchanges, auth keys,
|
||||
|
||||
@@ -379,6 +379,13 @@ func TestAPIv2OAuth_TaggedDeviceUser(t *testing.T) {
|
||||
assert.Equal(t, want, dev.User)
|
||||
assert.Equal(t, []string{"tag:ci"}, dev.Tags)
|
||||
|
||||
// oauthClientId names the join key, unless a token made it.
|
||||
if bearer == tok {
|
||||
assert.Empty(t, dev.OAuthClientID)
|
||||
} else {
|
||||
assert.Equal(t, key.ID, dev.OAuthClientID)
|
||||
}
|
||||
|
||||
if want == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user