mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
oidc: show requested tags on the registration confirmation page
This commit is contained in:
1 parent
c27a13eed8
commit
f0ecba2bd7
3 files changed
+137
-10
No files matched your search
+6
-1
@@ -947,6 +947,7 @@ func (a *AuthProviderOIDC) RegisterConfirmGetHandler(
|
||||
|
||||
if regData.Hostinfo != nil {
|
||||
info.OS = regData.Hostinfo.OS
|
||||
info.Tags = regData.Hostinfo.RequestTags
|
||||
}
|
||||
|
||||
content = templates.RegisterConfirm(info).Render()
|
||||
@@ -1116,7 +1117,7 @@ func (a *AuthProviderOIDC) handleRegistration(
|
||||
|
||||
func renderRegistrationSuccessTemplate(
|
||||
user *types.User,
|
||||
_ types.NodeView,
|
||||
node types.NodeView,
|
||||
reauth bool,
|
||||
) *bytes.Buffer {
|
||||
result := templates.AuthSuccessResult{
|
||||
@@ -1126,6 +1127,10 @@ func renderRegistrationSuccessTemplate(
|
||||
User: user.Display(),
|
||||
Message: "You can now close this window.",
|
||||
}
|
||||
if node.IsTagged() {
|
||||
result.User = strings.Join(node.Tags().AsSlice(), ", ")
|
||||
}
|
||||
|
||||
if !reauth {
|
||||
result.Title = "Headscale - Node Registered"
|
||||
result.Heading = "Node registered"
|
||||
|
||||
@@ -1800,6 +1800,117 @@ func TestRegisterConfirmShowsExistingNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterConfirmShowsRequestedTags(t *testing.T) {
|
||||
const ownerName = "tag-owner"
|
||||
|
||||
b := newOIDCBrowser(t)
|
||||
_, err := b.app.state.SetPolicy([]byte(`{
|
||||
"tagOwners": {"tag:prod": ["tag-owner@"]},
|
||||
"acls": [{"action":"accept", "src":["*"], "dst":["*:*"]}]
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine().Public()
|
||||
authID := types.MustAuthID()
|
||||
require.NoError(t, b.app.state.SetAuthCacheEntry(authID, types.NewRegisterAuthRequest(&types.RegistrationData{
|
||||
MachineKey: machineKey,
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "tagged-laptop",
|
||||
Hostinfo: &tailcfg.Hostinfo{RequestTags: []string{"tag:prod"}},
|
||||
})))
|
||||
b.idp.QueueUser(&mockoidc.MockUser{Subject: ownerName, PreferredUsername: ownerName})
|
||||
|
||||
status, landed, body := b.get(t, b.publicURL+"/register/"+authID.String())
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
|
||||
registered := regexp.MustCompile(`>Registered to</td><td[^>]*>([^<]*)</td>`).FindStringSubmatch(body)
|
||||
require.Len(t, registered, 2)
|
||||
assert.Equal(t, "tag:prod", registered[1])
|
||||
|
||||
approved := regexp.MustCompile(`>Approved by</td><td[^>]*>([^<]*)</td>`).FindStringSubmatch(body)
|
||||
if assert.Len(t, approved, 2) {
|
||||
assert.Equal(t, ownerName, approved[1])
|
||||
}
|
||||
|
||||
csrf := csrfInputRe.FindStringSubmatch(body)
|
||||
require.Len(t, csrf, 2)
|
||||
|
||||
//nolint:noctx,bodyclose // test response closed below
|
||||
confirmed, err := b.client.PostForm(landed.String(), url.Values{
|
||||
registerConfirmCSRFCookie: {csrf[1]},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
confirmedBody, err := io.ReadAll(confirmed.Body)
|
||||
confirmed.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, confirmed.StatusCode)
|
||||
|
||||
success := regexp.MustCompile(`Registered as <strong>([^<]*)</strong>`).FindSubmatch(confirmedBody)
|
||||
require.Len(t, success, 2)
|
||||
assert.Equal(t, "tag:prod", string(success[1]))
|
||||
|
||||
nodes := b.app.state.ListNodes()
|
||||
require.Equal(t, 1, nodes.Len())
|
||||
require.True(t, nodes.At(0).IsTagged())
|
||||
require.Equal(t, []string{"tag:prod"}, nodes.At(0).Tags().AsSlice())
|
||||
|
||||
reloginID := types.MustAuthID()
|
||||
require.NoError(t, b.app.state.SetAuthCacheEntry(reloginID, types.NewRegisterAuthRequest(&types.RegistrationData{
|
||||
MachineKey: machineKey,
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "different-client-name",
|
||||
})))
|
||||
b.idp.QueueUser(&mockoidc.MockUser{Subject: ownerName, PreferredUsername: ownerName})
|
||||
status, _, body = b.get(t, b.publicURL+"/register/"+reloginID.String())
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
|
||||
existing := regexp.MustCompile(`>Existing node</td><td[^>]*>([^<]*)</td>`).FindStringSubmatch(body)
|
||||
require.Len(t, existing, 2)
|
||||
require.Equal(t, "tagged-laptop", existing[1])
|
||||
require.True(t, b.app.state.ListNodes().At(0).IsTagged(), "viewing confirmation must not change ownership")
|
||||
}
|
||||
|
||||
func TestRegisterConfirmEscapesAndRejectsUnownedTags(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
tag string
|
||||
display string
|
||||
}{
|
||||
{tag: "tag:unowned", display: "tag:unowned"},
|
||||
{tag: "tag:<em>prod</em>", display: "tag:<em>prod</em>"},
|
||||
} {
|
||||
t.Run(tt.tag, func(t *testing.T) {
|
||||
b := newOIDCBrowser(t)
|
||||
authID := types.MustAuthID()
|
||||
require.NoError(t, b.app.state.SetAuthCacheEntry(authID, types.NewRegisterAuthRequest(&types.RegistrationData{
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "unowned-tag-node",
|
||||
Hostinfo: &tailcfg.Hostinfo{RequestTags: []string{tt.tag}},
|
||||
})))
|
||||
|
||||
status, landed, body := b.get(t, b.publicURL+"/register/"+authID.String())
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
|
||||
row := regexp.MustCompile(`>Registered to</td><td[^>]*>(.*?)</td>`).FindStringSubmatch(body)
|
||||
require.Len(t, row, 2)
|
||||
assert.Equal(t, tt.display, row[1])
|
||||
|
||||
csrf := csrfInputRe.FindStringSubmatch(body)
|
||||
require.Len(t, csrf, 2)
|
||||
|
||||
//nolint:noctx,bodyclose // test response closed below
|
||||
confirmed, err := b.client.PostForm(landed.String(), url.Values{
|
||||
registerConfirmCSRFCookie: {csrf[1]},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
confirmed.Body.Close()
|
||||
require.NotEqual(t, http.StatusOK, confirmed.StatusCode)
|
||||
require.Zero(t, b.app.state.ListNodes().Len())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCRegistrationRejectsOversizedMergedHostinfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package templates
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"strings"
|
||||
|
||||
"github.com/chasefleming/elem-go"
|
||||
"github.com/chasefleming/elem-go/attrs"
|
||||
@@ -46,6 +47,9 @@ type RegisterConfirmInfo struct {
|
||||
|
||||
// ExistingNode is the stored node name, empty for a new device.
|
||||
ExistingNode string
|
||||
|
||||
// Tags are the ownership tags requested by the registering client.
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// RegisterConfirm renders an interstitial page that asks the
|
||||
@@ -55,15 +59,22 @@ type RegisterConfirmInfo struct {
|
||||
// silently complete a phishing-style registration when the victim's
|
||||
// IdP allows silent SSO.
|
||||
func RegisterConfirm(info RegisterConfirmInfo) *elem.Element {
|
||||
deviceList := deviceTable(
|
||||
[]deviceRow{
|
||||
{"Hostname", elem.Text(info.Hostname)},
|
||||
{"OS", elem.Text(cmp.Or(info.OS, "(unknown)"))},
|
||||
{"Machine key", Code(elem.Text(info.MachineKey))},
|
||||
{"Existing node", elem.Text(cmp.Or(info.ExistingNode, "None, this is a new device"))},
|
||||
{"Registered to", elem.Text(info.User)},
|
||||
},
|
||||
)
|
||||
rows := []deviceRow{
|
||||
{"Hostname", elem.Text(info.Hostname)},
|
||||
{"OS", elem.Text(cmp.Or(info.OS, "(unknown)"))},
|
||||
{"Machine key", Code(elem.Text(info.MachineKey))},
|
||||
{"Existing node", elem.Text(cmp.Or(info.ExistingNode, "None, this is a new device"))},
|
||||
}
|
||||
if len(info.Tags) > 0 {
|
||||
rows = append(rows,
|
||||
deviceRow{"Registered to", elem.Text(strings.Join(info.Tags, ", "))},
|
||||
deviceRow{"Approved by", elem.Text(info.User)},
|
||||
)
|
||||
} else {
|
||||
rows = append(rows, deviceRow{"Registered to", elem.Text(info.User)})
|
||||
}
|
||||
|
||||
deviceList := deviceTable(rows)
|
||||
|
||||
return page(
|
||||
"Headscale - Confirm node registration",
|
||||
|
||||
Reference in new issue
Block a user