mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-26 22:39:07 +01:00
The mobile bridges only carried the policy fetcher, leaving every enforcement decision to the native apps: the desktop derived its UI restrictions in the Wails service layer, the daemon kept the conflict machinery in the server package, and both mobile bridges duplicated the JSON fetch adapter. Anything the native side had to reimplement was a place for iOS and Android to drift apart. Enforcement now lives in client/mdm and is consumed identically by all three platforms: - conflicts.go holds the value-aware conflict checks lifted out of the daemon, so the same normalization (canonical URLs, PSK sentinel echo) applies wherever a config change is validated. - restrictions.go derives the UI enforcement snapshot from a policy and renders it in the JSON shape the desktop frontend already consumes. The service-layer types become aliases, keeping one source of truth. - jsonloader.go replaces the adapter that was copy-pasted into both bridges. - changedetector.go moves change detection off the native side: the caller forwards the OS notification and asks whether the managed configuration actually changed, instead of diffing dictionaries itself. The mobile bridges gain the enforcement the daemon already had. The Preferences getters resolve managed keys from the policy, so a naive UI shows the enforced value; Commit rejects a staged change that diverges from a managed key; NewAuth resolves the managed management URL before persisting the config and overlays the policy on it, so a login can no longer run against a URL the policy forbids. Android's profile mutations fail closed when disableProfiles is set. NewAuth takes the fetcher as a required argument rather than keeping a policy-blind overload: the apps consume this code as a submodule, so a compile error at the bump is the point. The mobile PSK getter is replaced by a presence check — the key has no reason to cross the bridge, and not returning it means the native side needs no redaction sentinel of its own.
252 lines
7.8 KiB
Go
252 lines
7.8 KiB
Go
package android
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/auth"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/mdm"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
)
|
|
|
|
// SSOListener is async listener for mobile framework
|
|
type SSOListener interface {
|
|
OnSuccess(bool)
|
|
OnError(error)
|
|
}
|
|
|
|
// ErrListener is async listener for mobile framework
|
|
type ErrListener interface {
|
|
OnSuccess()
|
|
OnError(error)
|
|
}
|
|
|
|
// URLOpener it is a callback interface. The Open function will be triggered if
|
|
// the backend want to show an url for the user
|
|
type URLOpener interface {
|
|
Open(url string, userCode string)
|
|
OnLoginSuccess()
|
|
}
|
|
|
|
// Auth can register or login new client
|
|
type Auth struct {
|
|
ctx context.Context
|
|
config *profilemanager.Config
|
|
cfgPath string
|
|
}
|
|
|
|
// NewAuth instantiate Auth struct and validate the management URL
|
|
//
|
|
// The configuration at cfgPath is reused when one is already there, and only created when it is
|
|
// not. Building a fresh in-memory config unconditionally gives the client a new WireGuard key on
|
|
// every call: the peer registers under that key, the key is written out, and any peer registered by
|
|
// an earlier call is orphaned on the server. It also breaks a client that enrols and then runs from
|
|
// the persisted config, because the identity it registered is not the one it runs with — the
|
|
// management stream rejects it with "no peer auth method provided".
|
|
//
|
|
// Auth is constructed under the active MDM policy: a managed management URL
|
|
// replaces the caller-supplied one before the config is persisted, and the
|
|
// policy is overlaid on the resolved config so the login runs against the
|
|
// enforced values. A nil fetcher disables MDM enforcement.
|
|
func NewAuth(cfgPath string, mgmURL string, fetcher PolicyFetcher) (*Auth, error) {
|
|
policy := loaderFor(fetcher).Load()
|
|
if v, ok := policy.GetString(mdm.KeyManagementURL); ok {
|
|
mgmURL = v
|
|
}
|
|
inputCfg := profilemanager.ConfigInput{
|
|
ConfigPath: cfgPath,
|
|
ManagementURL: mgmURL,
|
|
}
|
|
|
|
cfg, err := profilemanager.UpdateOrCreateConfig(inputCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.ApplyMDMPolicy(policy)
|
|
|
|
return &Auth{
|
|
ctx: context.Background(),
|
|
config: cfg,
|
|
cfgPath: cfgPath,
|
|
}, nil
|
|
}
|
|
|
|
// NewAuthWithConfig instantiate Auth based on existing config. cfgPath is the
|
|
// file the config was loaded from; it identifies the profile whose account email
|
|
// backs the login_hint.
|
|
func NewAuthWithConfig(ctx context.Context, config *profilemanager.Config, cfgPath string) *Auth {
|
|
return &Auth{
|
|
ctx: ctx,
|
|
config: config,
|
|
cfgPath: cfgPath,
|
|
}
|
|
}
|
|
|
|
// SaveConfigIfSSOSupported test the connectivity with the management server by retrieving the server device flow info.
|
|
// If it returns a flow info than save the configuration and return true. If it gets a codes.NotFound, it means that SSO
|
|
// is not supported and returns false without saving the configuration. For other errors return false.
|
|
func (a *Auth) SaveConfigIfSSOSupported(listener SSOListener) {
|
|
go func() {
|
|
sso, err := a.saveConfigIfSSOSupported()
|
|
if err != nil {
|
|
listener.OnError(err)
|
|
} else {
|
|
listener.OnSuccess(sso)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
|
|
authClient, err := auth.NewAuth(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to create auth client: %v", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
supportsSSO, err := authClient.IsSSOSupported(a.ctx)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check SSO support: %v", err)
|
|
}
|
|
|
|
if !supportsSSO {
|
|
return false, nil
|
|
}
|
|
|
|
err = profilemanager.WriteOutConfig(a.cfgPath, a.config)
|
|
return true, err
|
|
}
|
|
|
|
// LoginWithSetupKeyAndSaveConfig test the connectivity with the management server with the setup key.
|
|
func (a *Auth) LoginWithSetupKeyAndSaveConfig(resultListener ErrListener, setupKey string, deviceName string) {
|
|
go func() {
|
|
err := a.loginWithSetupKeyAndSaveConfig(setupKey, deviceName)
|
|
if err != nil {
|
|
resultListener.OnError(err)
|
|
} else {
|
|
resultListener.OnSuccess()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string) error {
|
|
authClient, err := auth.NewAuth(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth client: %v", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
//nolint
|
|
ctxWithValues := context.WithValue(a.ctx, system.DeviceNameCtxKey, deviceName)
|
|
err, _ = authClient.Login(ctxWithValues, setupKey, "")
|
|
if err != nil {
|
|
return fmt.Errorf("login failed: %v", err)
|
|
}
|
|
|
|
return profilemanager.WriteOutConfig(a.cfgPath, a.config)
|
|
}
|
|
|
|
// Login try register the client on the server
|
|
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener, isAndroidTV bool) {
|
|
go func() {
|
|
err := a.login(urlOpener, isAndroidTV)
|
|
if err != nil {
|
|
resultListener.OnError(err)
|
|
} else {
|
|
resultListener.OnSuccess()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Auth) login(urlOpener URLOpener, isAndroidTV bool) error {
|
|
authClient, err := auth.NewAuth(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth client: %v", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
// check if we need to generate JWT token
|
|
needsLogin, err := authClient.IsLoginRequired(a.ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check login requirement: %v", err)
|
|
}
|
|
|
|
jwtToken := ""
|
|
email := ""
|
|
if needsLogin {
|
|
tokenInfo, err := a.foregroundGetTokenInfo(authClient, urlOpener, isAndroidTV)
|
|
if err != nil {
|
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
|
}
|
|
jwtToken = tokenInfo.GetTokenToUse()
|
|
email = tokenInfo.Email
|
|
}
|
|
|
|
err, _ = authClient.Login(a.ctx, "", jwtToken)
|
|
if err != nil {
|
|
return fmt.Errorf("login failed: %v", err)
|
|
}
|
|
|
|
// Stored after Login, not before: a rejected token must not leave a hint
|
|
// pointing at an account that cannot be used.
|
|
if email != "" && a.cfgPath != "" {
|
|
if err := writeProfileEmail(a.cfgPath, email); err != nil {
|
|
log.Warnf("failed to store profile account email: %v", err)
|
|
}
|
|
}
|
|
|
|
go urlOpener.OnLoginSuccess()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Auth) foregroundGetTokenInfo(authClient *auth.Auth, urlOpener URLOpener, isAndroidTV bool) (*auth.TokenInfo, error) {
|
|
oAuthFlow, err := authClient.GetOAuthFlow(a.ctx, isAndroidTV, profileLoginHint(a.cfgPath))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get OAuth flow: %v", err)
|
|
}
|
|
|
|
return runOAuthFlow(a.ctx, oAuthFlow, urlOpener, nil)
|
|
}
|
|
|
|
// profileLoginHint returns the stored account email for the profile at cfgPath.
|
|
// An empty hint is deliberate, not a fallback: a fresh profile leaves the
|
|
// choice to the IdP. Switching accounts is done by switching or removing
|
|
// profiles, not by logging out — logout keeps the email.
|
|
func profileLoginHint(cfgPath string) string {
|
|
if cfgPath == "" {
|
|
return ""
|
|
}
|
|
return readProfileEmail(cfgPath)
|
|
}
|
|
|
|
// runOAuthFlow drives an already acquired OAuth flow to a token: requests the
|
|
// flow info, presents the verification URL through the opener and waits for
|
|
// the browser round-trip. Open is called synchronously — it is what marks the
|
|
// surface as opened on the client side, and a fast token's OnLoginSuccess is
|
|
// a no-op until it has, so the dismissal would be dropped rather than
|
|
// delayed. Openers must therefore not block: they post their UI work and
|
|
// return. onWaiting, when set, runs after the URL is shown, right before the
|
|
// blocking wait.
|
|
func runOAuthFlow(ctx context.Context, flow auth.OAuthFlow, urlOpener URLOpener, onWaiting func()) (*auth.TokenInfo, error) {
|
|
flowInfo, err := flow.RequestAuthInfo(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request auth info: %w", err)
|
|
}
|
|
|
|
urlOpener.Open(flowInfo.VerificationURIComplete, flowInfo.UserCode)
|
|
|
|
if onWaiting != nil {
|
|
onWaiting()
|
|
}
|
|
|
|
tokenInfo, err := flow.WaitToken(ctx, flowInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wait for token: %w", err)
|
|
}
|
|
|
|
return &tokenInfo, nil
|
|
}
|