Compare commits

..

4 commits

Author SHA1 Message Date
Kristoffer Dalby
21e8f87845
Merge 633297915a into 6275399327 2024-11-19 09:22:35 +00:00
Kristoffer Dalby
633297915a
only set username and email if valid
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-19 10:22:26 +01:00
Kristoffer Dalby
4ba319a0d3
ensure provider id is found out of order
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-19 09:50:53 +01:00
Kristoffer Dalby
b2ab5ac1ad
resolve user identifier to stable ID
currently, the policy approach node to user matching
with a quite naive approach looking at the username
provided in the policy and matched it with the username
on the nodes. This worked ok as long as usernames were
unique and did not change.

As usernames are no longer guarenteed to be unique in
an OIDC environment we cant rely on this.

This changes the mechanism that matches the user string
(now user token) with nodes:

- first find all potential users by looking up:
  - database ID
  - provider ID (OIDC)
  - username/email

If more than one user is matching, then the query is
rejected, and zero matching nodes are returned.

When a single user is found, the node is matched against
the User database ID, which are also present on the actual
node.

This means that from this commit, users can use the following
to identify users in the policy:
- provider identity (iss + sub)
- username
- email
- database id

There are more changes coming to this, so it is not recommended
to start using any of these new abilities, with the exception
of email, which will not change since it includes an @.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-11-19 09:50:53 +01:00
3 changed files with 10 additions and 9 deletions

View file

@ -990,7 +990,7 @@ func filterNodesByUser(nodes types.Nodes, users []types.User, userToken string)
var potentialUsers []types.User
for _, user := range users {
if user.ProviderIdentifier == userToken {
if user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == userToken {
// If a user is matching with a known unique field,
// disgard all other users and only keep the current
// user.

View file

@ -1,6 +1,7 @@
package policy
import (
"database/sql"
"errors"
"math/rand/v2"
"net/netip"
@ -873,7 +874,7 @@ func Test_filterNodesByUser(t *testing.T) {
users := []types.User{
{Model: gorm.Model{ID: 1}, Name: "marc"},
{Model: gorm.Model{ID: 2}, Name: "joe", Email: "joe@headscale.net"},
{Model: gorm.Model{ID: 3}, Name: "mikael", Email: "mikael@headscale.net", ProviderIdentifier: "http://oidc.org/1234"},
{Model: gorm.Model{ID: 3}, Name: "mikael", Email: "mikael@headscale.net", ProviderIdentifier: sql.NullString{String: "http://oidc.org/1234", Valid: true}},
{Model: gorm.Model{ID: 4}, Name: "mikael2", Email: "mikael@headscale.net"},
{Model: gorm.Model{ID: 5}, Name: "mikael", Email: "mikael2@headscale.net"},
{Model: gorm.Model{ID: 6}, Name: "http://oidc.org/1234", Email: "mikael@headscale.net"},

View file

@ -24,14 +24,14 @@ type User struct {
// Username for the user, is used if email is empty
// Should not be used, please use Username().
Name sql.NullString `gorm:"unique"`
Name string `gorm:"unique"`
// Typically the full name of the user
DisplayName string
// Email of the user
// Should not be used, please use Username().
Email sql.NullString
Email string
// Unique identifier of the user from OIDC,
// comes from `sub` claim in the OIDC token
@ -53,7 +53,7 @@ type User struct {
// should be used throughout headscale, in information returned to the
// user and the Policy engine.
func (u *User) Username() string {
return cmp.Or(u.Email.String, u.Name.String, u.ProviderIdentifier.String, strconv.FormatUint(uint64(u.ID), 10))
return cmp.Or(u.Email, u.Name, u.ProviderIdentifier.String, strconv.FormatUint(uint64(u.ID), 10))
}
// DisplayNameOrUsername returns the DisplayName if it exists, otherwise
@ -105,10 +105,10 @@ func (u *User) TailscaleUserProfile() tailcfg.UserProfile {
func (u *User) Proto() *v1.User {
return &v1.User{
Id: strconv.FormatUint(uint64(u.ID), util.Base10),
Name: u.Name.String,
Name: u.Name,
CreatedAt: timestamppb.New(u.CreatedAt),
DisplayName: u.DisplayName,
Email: u.Email.String,
Email: u.Email,
ProviderId: u.ProviderIdentifier.String,
Provider: u.Provider,
ProfilePicUrl: u.ProfilePicURL,
@ -133,13 +133,13 @@ type OIDCClaims struct {
func (u *User) FromClaim(claims *OIDCClaims) {
err := util.CheckForFQDNRules(claims.Username)
if err == nil {
u.Name.String = claims.Username
u.Name = claims.Username
}
if claims.EmailVerified {
_, err = mail.ParseAddress(claims.Email)
if err == nil {
u.Email.String = claims.Email
u.Email = claims.Email
}
}