Files
zeekayandHanzo Dev d8faf52e7e Assert the name the fixture actually carries
Renaming the placeholder in the claim set left the two assertions below it
still reading Zach Kelling, so the one gate this repository declares has been
red since 2026-09-01 and says so the moment anything runs it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:46:34 -07:00

226 lines
8.4 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package oauthhanzo
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/hanzoai/authz"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/mlog"
"github.com/hanzoteam/server/server/public/shared/request"
)
func TestIAMUserIsValid(t *testing.T) {
valid := func() *IAMUser {
return &IAMUser{Sub: "u-1", Email: "[email protected]", Owner: "hanzo"}
}
t.Run("accepts a complete claim set", func(t *testing.T) {
require.NoError(t, valid().IsValid())
})
t.Run("rejects a missing subject", func(t *testing.T) {
u := valid()
u.Sub = ""
require.Error(t, u.IsValid())
})
t.Run("rejects a missing email", func(t *testing.T) {
u := valid()
u.Email = ""
require.Error(t, u.IsValid())
})
t.Run("rejects a missing owner, because there is no tenant to place them in", func(t *testing.T) {
u := valid()
u.Owner = ""
require.Error(t, u.IsValid())
})
}
func TestUserFromIAMUser(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
t.Run("maps the claims onto a user", func(t *testing.T) {
user := userFromIAMUser(logger, &IAMUser{
Sub: "u-42",
Email: "[email protected]",
EmailVerified: true,
PreferredUsername: "zeekay",
DisplayName: "Grace Hopper",
Owner: "hanzo",
}, nil)
require.Equal(t, "zeekay", user.Username)
require.Equal(t, "[email protected]", user.Email, "email should be lowercased")
require.True(t, user.EmailVerified)
require.Equal(t, "Grace", user.FirstName)
require.Equal(t, "Hopper", user.LastName)
require.Equal(t, "u-42", *user.AuthData, "sub is the stable identifier")
require.Equal(t, model.UserAuthServiceHanzo, user.AuthService)
org, ok := user.GetProp(model.UserPropOrg)
require.True(t, ok)
require.Equal(t, "hanzo", org)
})
t.Run("takes the local part when preferred_username is an address", func(t *testing.T) {
user := userFromIAMUser(logger, &IAMUser{
Sub: "u-1", Email: "[email protected]", PreferredUsername: "[email protected]", Owner: "hanzo",
}, nil)
require.Equal(t, "zeekay", user.Username)
})
t.Run("falls back to name when preferred_username is absent", func(t *testing.T) {
user := userFromIAMUser(logger, &IAMUser{
Sub: "u-1", Email: "[email protected]", Name: "zeekay", Owner: "hanzo",
}, nil)
require.Equal(t, "zeekay", user.Username)
})
t.Run("keeps a multi-word surname whole", func(t *testing.T) {
user := userFromIAMUser(logger, &IAMUser{
Sub: "u-1", Email: "[email protected]", PreferredUsername: "a", DisplayName: "Ada Van Der Berg", Owner: "hanzo",
}, nil)
require.Equal(t, "Ada", user.FirstName)
require.Equal(t, "Van Der Berg", user.LastName)
})
t.Run("handles a single-word display name", func(t *testing.T) {
user := userFromIAMUser(logger, &IAMUser{
Sub: "u-1", Email: "[email protected]", PreferredUsername: "a", DisplayName: "Ada", Owner: "hanzo",
}, nil)
require.Equal(t, "Ada", user.FirstName)
require.Empty(t, user.LastName)
})
}
func TestProviderGetUserFromJSON(t *testing.T) {
p := &Provider{}
rctx := request.TestContext(t)
t.Run("reads a UserInfo response", func(t *testing.T) {
body := `{"sub":"u-7","email":"[email protected]","email_verified":true,
"preferred_username":"zeekay","displayName":"Grace Hopper",
"owner":"hanzo","organization":"Hanzo AI"}`
user, err := p.GetUserFromJSON(rctx, strings.NewReader(body), nil, nil)
require.NoError(t, err)
require.Equal(t, "[email protected]", user.Email)
org, _ := user.GetProp(model.UserPropOrg)
require.Equal(t, "hanzo", org, "the owner claim is the tenant, not organization")
})
t.Run("refuses a response with no owner", func(t *testing.T) {
body := `{"sub":"u-7","email":"[email protected]","preferred_username":"zeekay"}`
_, err := p.GetUserFromJSON(rctx, strings.NewReader(body), nil, nil)
require.Error(t, err)
})
t.Run("refuses malformed JSON", func(t *testing.T) {
_, err := p.GetUserFromJSON(rctx, strings.NewReader("{"), nil, nil)
require.Error(t, err)
})
}
func TestProviderIsSameUser(t *testing.T) {
p := &Provider{}
rctx := request.TestContext(t)
id := func(s string) *model.User { return &model.User{AuthData: &s} }
require.True(t, p.IsSameUser(rctx, id("u-1"), id("u-1")))
require.False(t, p.IsSameUser(rctx, id("u-1"), id("u-2")))
require.False(t, p.IsSameUser(rctx, &model.User{}, id("u-1")), "a nil auth data must never match")
sub := func(s string) *string { return &s }
signingIn := &model.User{AuthService: model.UserAuthServiceHanzo, AuthData: sub("u-9"), Email: "[email protected]"}
password := &model.User{Email: "[email protected]"}
require.True(t, p.IsSameUser(rctx, password, signingIn), "an account with a password is the person hanzo.id vouches for")
strangerAddress := &model.User{Email: "[email protected]"}
require.False(t, p.IsSameUser(rctx, strangerAddress, signingIn))
require.False(t, p.IsSameUser(rctx, &model.User{}, &model.User{}), "two blank users are not each other")
otherSubject := &model.User{AuthService: model.UserAuthServiceHanzo, AuthData: sub("u-8"), Email: "[email protected]"}
require.False(t, p.IsSameUser(rctx, otherSubject, signingIn), "an identity never moves from one subject to another")
}
func TestProviderReadsTheHanzoSlot(t *testing.T) {
p := &Provider{}
cfg := &model.Config{}
cfg.SetDefaults()
settings, err := p.GetSSOSettings(request.TestContext(t), cfg, model.UserAuthServiceHanzo)
require.NoError(t, err)
require.Same(t, &cfg.HanzoSettings, settings)
require.Equal(t, model.HanzoSettingsDefaultAuthEndpoint, *settings.AuthEndpoint)
}
// Who administers this server is IAM's answer. These cases are the boundary, so
// each one names what it would cost to get wrong.
func TestUserFromIAMUserDerivesAuthority(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(t)
admin := model.SystemAdminRoleId + " " + model.SystemUserRoleId
person := func(orgs []authz.Membership) *model.User {
return userFromIAMUser(logger, &IAMUser{
Sub: "u-1", Email: "[email protected]", PreferredUsername: "z", Owner: "hanzo", Orgs: orgs,
}, nil)
}
t.Run("an operator anchored in a brand org", func(t *testing.T) {
// The case that matters and the one a home-org read misses: an operator is
// anchored where they do ordinary work and holds the reserved org alongside
// it. Reading `owner` here sees "hanzo" and grants nothing.
u := person([]authz.Membership{{Org: "hanzo", Role: "admin"}, {Org: "admin", Role: "admin"}})
require.Equal(t, admin, u.Roles)
})
t.Run("the reserved org at any position", func(t *testing.T) {
u := person([]authz.Membership{{Org: "lux", Role: "member"}, {Org: "zoo", Role: "member"}, {Org: "admin", Role: "admin"}})
require.Equal(t, admin, u.Roles, "position in the set carries no meaning")
})
t.Run("an org admin is not a platform admin", func(t *testing.T) {
// Administering your own org is self-service. If this ever grants platform
// authority, every customer who administers their own tenant administers ours.
u := person([]authz.Membership{{Org: "hanzo", Role: "owner"}, {Org: "lux", Role: "admin"}})
require.Equal(t, model.SystemUserRoleId, u.Roles)
})
t.Run("a lookalike org is not the reserved one", func(t *testing.T) {
// An org someone can self-serve must never be able to spell the reserved one.
for _, org := range []string{"Admin", "ADMIN", "admin ", " admin", "admins", "admin-org"} {
u := person([]authz.Membership{{Org: "hanzo", Role: "member"}, {Org: org, Role: "admin"}})
require.Equal(t, model.SystemUserRoleId, u.Roles, "org %q must not read as the reserved org", org)
}
})
t.Run("no membership set grants nothing", func(t *testing.T) {
// An IAM that does not send `orgs`, or a machine identity. Failing closed here
// is what lets this ship before or after the issuer starts sending the claim.
require.Equal(t, model.SystemUserRoleId, person(nil).Roles)
require.Equal(t, model.SystemUserRoleId, person([]authz.Membership{}).Roles)
})
t.Run("the claim is read off the wire", func(t *testing.T) {
// The whole derivation is worthless if the JSON key does not match what IAM
// emits, and a struct tag is exactly the kind of thing no other test covers.
u, err := (&Provider{}).GetUserFromJSON(request.TestContext(t), strings.NewReader(
`{"sub":"u-1","email":"[email protected]","preferred_username":"z","owner":"hanzo",
"orgs":[{"org":"hanzo","role":"admin"},{"org":"admin","role":"admin"}]}`), nil, nil)
require.NoError(t, err)
require.Equal(t, admin, u.Roles)
})
}