mirror of
https://github.com/hanzoai/team.git
synced 2026-11-01 07:55:31 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
140 lines
4.6 KiB
Go
140 lines
4.6 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package oauthhanzo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
|
|
"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"
|
|
"github.com/hanzoteam/server/server/v8/einterfaces"
|
|
)
|
|
|
|
// Provider reads Hanzo IAM's UserInfo response. The claim set is the one
|
|
// hanzo.id advertises at /.well-known/openid-configuration.
|
|
type Provider struct{}
|
|
|
|
type IAMUser struct {
|
|
Sub string `json:"sub"`
|
|
Email string `json:"email"`
|
|
EmailVerified bool `json:"email_verified"`
|
|
Name string `json:"name"`
|
|
PreferredUsername string `json:"preferred_username"`
|
|
DisplayName string `json:"displayName"`
|
|
Picture string `json:"picture"`
|
|
|
|
// Owner is the IAM org slug and the tenant boundary. Organization is its
|
|
// display name, which is presentational only -- never key on it.
|
|
Owner string `json:"owner"`
|
|
Organization string `json:"organization"`
|
|
|
|
// Orgs is the set of orgs this person is a member of, home org first. Owner
|
|
// says where they are ANCHORED; this says what they may act in, and the two
|
|
// are different questions -- an operator is anchored in an ordinary brand org
|
|
// and holds the reserved org alongside it.
|
|
Orgs []authz.Membership `json:"orgs"`
|
|
}
|
|
|
|
func init() {
|
|
einterfaces.RegisterOAuthProvider(model.UserAuthServiceHanzo, &Provider{})
|
|
}
|
|
|
|
func (u *IAMUser) IsValid() error {
|
|
if u.Sub == "" {
|
|
return errors.New("hanzo iam: subject claim is empty")
|
|
}
|
|
if u.Email == "" {
|
|
return errors.New("hanzo iam: email claim is empty")
|
|
}
|
|
if u.Owner == "" {
|
|
return errors.New("hanzo iam: owner claim is empty, cannot place the user in a tenant")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func userFromIAMUser(logger mlog.LoggerIFace, iu *IAMUser, settings *model.SSOSettings) *model.User {
|
|
user := &model.User{}
|
|
|
|
name := iu.PreferredUsername
|
|
if name == "" {
|
|
name = iu.Name
|
|
}
|
|
// A preferred_username may be an address; the local part is the username.
|
|
user.Username = model.CleanUsername(logger, strings.Split(name, "@")[0])
|
|
|
|
display := iu.DisplayName
|
|
if display == "" {
|
|
display = iu.Name
|
|
}
|
|
if first, last, ok := strings.Cut(display, " "); ok {
|
|
user.FirstName, user.LastName = first, last
|
|
} else {
|
|
user.FirstName = display
|
|
}
|
|
|
|
user.Email = strings.ToLower(iu.Email)
|
|
user.EmailVerified = iu.EmailVerified
|
|
|
|
// sub is IAM's stable identifier. Email and username both move; sub does not.
|
|
sub := iu.Sub
|
|
user.AuthData = &sub
|
|
user.AuthService = model.UserAuthServiceHanzo
|
|
|
|
user.SetProp(model.UserPropOrg, iu.Owner)
|
|
|
|
// Who administers this server is IAM's answer, and it is asked through IAM's
|
|
// own predicate rather than restated here: platform authority is membership of
|
|
// the reserved org, held at any position, which is not the same question as
|
|
// which org someone is anchored in. Re-deriving it locally is how a check that
|
|
// reads correctly comes to disagree with the issuer.
|
|
//
|
|
// An identity carrying no membership set -- a machine token, or an IAM that
|
|
// does not send `orgs` -- is not an operator, so this fails closed.
|
|
user.Roles = model.SystemUserRoleId
|
|
if (&authz.Claims{Orgs: iu.Orgs}).PlatformSudo() {
|
|
user.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
func (p *Provider) GetUserFromJSON(rctx request.CTX, data io.Reader, tokenUser *model.User, settings *model.SSOSettings) (*model.User, error) {
|
|
var iu IAMUser
|
|
if err := json.NewDecoder(data).Decode(&iu); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := iu.IsValid(); err != nil {
|
|
return nil, err
|
|
}
|
|
return userFromIAMUser(rctx.Logger(), &iu, settings), nil
|
|
}
|
|
|
|
func (p *Provider) GetSSOSettings(_ request.CTX, config *model.Config, service string) (*model.SSOSettings, error) {
|
|
return &config.HanzoSettings, nil
|
|
}
|
|
|
|
func (p *Provider) GetUserFromIdToken(_ request.CTX, idToken string) (*model.User, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (p *Provider) IsSameUser(_ request.CTX, dbUser, oauthUser *model.User) bool {
|
|
if dbUser.AuthData != nil && oauthUser.AuthData != nil && *dbUser.AuthData == *oauthUser.AuthData {
|
|
return true
|
|
}
|
|
// An account already carrying a different subject stays with it: an
|
|
// identity is never moved from one subject to another.
|
|
if dbUser.AuthService == model.UserAuthServiceHanzo {
|
|
return false
|
|
}
|
|
// hanzo.id issues every identity here and vouches for the address, so the
|
|
// account holding it is this user, whatever it signed in with before.
|
|
return oauthUser.Email != "" && dbUser.Email == oauthUser.Email
|
|
}
|