Files
Vishal Dalwadi c178a617f8 NM-341-v6: Netmaker Desktop Changes (#4110)
* wip(go): pass ctx during license validation;

* wip(go): add todo for making changes later; run hook separately for each tenant;

* wip(go): return specific errors when license validation response not in cache;

* wip(go): use retryable.httpclient for calling license validation api;

* wip(go): process license validation;

* wip(go): license validation function;

* wip(go): migration on license validation;

* wip(go): org settings not expected;

* wip(go): remove expiry from license validation response;

* wip(go): add limits and limits enforcement to license validation response;

* wip(go): remove expiry usages;

* wip(go): add arg to clear cache;

* wip(go): add arg to clear cache;

* wip(go): skip license validation if cached resp;

* wip(go): allow org_id and tenant_id query params;

* wip(go): upsert tenant membership on sync;

* wip(go): pass ctx to delete network roles;

* wip(go): create tenant membership on org owner creation on ce;

* wip(go): add tenant orchestrator;

* wip(go): use tenant orchestrator;

* wip(go): add default settings for tenant;

* wip(go): make org owner superadmin on all tenants;

* wip(go): simplify SyncOrgAndTenants;

* wip(go): retry license validation only 3 times

* wip(go): store invalid err;

* wip(go): add function to check if limits are to be enforced on a tenant;

* wip(go): pass tenant scope if possible when checking for license validation error;

* wip(go): remove merge errors;

* wip(go): add functions for limit checks;

* wip(go): wire host limit check;

* wip(go): wire ingress limit check;

* wip(go): wire egress limit check;

* wip(go): wire client limit check;

* wip(go): wire network limit check;

* wip(go): wire user limit check;

* wip(go): use correct errors;

* wip(go): skip telemetry for msps;

* wip(go): disable telemetry on msp tenants;

* wip(go): err check;

* wip(go): uncomment log event;

* wip(go): get sso state to get scope for non-headless oauth login;

* wip(go): inline delete pending user;

* wip(go): allow both org and tenant scope for pending users table;

* wip(go): add migration for pending user table during set tenant id and rekey org/tenant id;

* wip(go): update queries to use new scope fields in pending users table;

* wip(go): allow both org and tenant scope for user invites table;

* wip(go): update queries to use new scope fields;

* wip(go): add migration for rekey-ing org/tenant id and setting tenant id;

* wip(go): inline delete user invite;

* wip(go): set scope when creating invite;

* wip(go): get user count with membership is ctx is tenant scoped;

* wip(go): add methods to upsert and delete org memberships;

* wip(go): add global valid tenant and org roles map;

* wip(go): add scope query in user invite get by email;

* wip(go): add support for upsert and delete of org and tenant membership both;

* wip(go): infer scope for user invite apis;

* wip(go): wire ctx in get user invite by email;

* wip(go): allow no scope headers to be passed in infer scope apis;

* wip(go): add validation and checks for org-scoped invite api handlers;

* wip(go): wire ctx to is pending user check;

* wip(go): infer scope for pending user apis;

* wip(go): set default role based on ctx scope;

* wip(go): move mfa to tenant membership table; move account status to membership tables;

* wip(go): migrate account status and mfa settings;

* wip(go): update account status on sync;

* wip(go): use update mfa to update mfa setting;

* wip(go): set new membership settings on user creation;

* wip(go): use membership where required;

* wip(go): wire ctx on key unlink refs;

* wip(go): use r.Context directly without reinitializing db;

* wip(go): wire ctx in acls;

* wip(go): add tenant scoped primary key constructor;

* wip(go): use tenant scoped primary key for acls, dns, extclients and tags;

* wip(go): wire ctx for get groups in network;

* wip(go): wire ctx for get groups;

* wip(go): scope non-uuid group ids by tenant id;

* wip(go): migrate non-uuid group ids to scoped ids;

* wip(go): don't expect scope id in invite validation and signup apis;

* wip(go): wire ctx for get matching user; allow org scope when querying by external idp id;

* wip(go): add mfa to org;

* wip(go): scope preauth token;

* wip(go): add fallback for older auth tokens;

* wip(go): wire ctx for preauth token creation;

* wip(go): migrate mfa settings;

* wip(go): resolve mfa settings for inherited auth;

* wip(go): update mfa for org;

* wip(go): infer scope for totp apis;

* wip(go): use ctx when passed;

* wip(go): infer scope for list user apis;

* wip(go): ensure set primary keys don't cause errors during fetch;

* wip(go): make primary key for integrations uuid and store provider in separate key;

* wip(go): migrate integration ids;

* wip(go): use provider instead of id everywhere;

* wip(go): remove gorm column;

* wip(go): wire ctx for igw egress creation;

* wip(go): wire ctx for igw egress update;

* wip(go): wire ctx for host registration;

* wip(go): wire ctx for host registration;

* wip(go): merge errors;

* wip(go): merge errors;

* wip(go): wire ctx; remove default scope usages;

* wip(go): use get with membership;

* wip(go): org scope license validation;

* wip(go): remove debug log;

* wip(go): remove scoping on list roles;

* wip(go): ignore invite creation if already exists;

* wip(go): add is msp func to check if deployment is msp deployment;

* wip(go): ensure username is email for msp users;

* wip(go): add email validated check on user table;

* wip(go): mark email validated when oauth user;

* wip(go): handle gorm error;

* wip(go): add tenant id to siem grpc schema;

* wip(go): read tenant from scoped ctx;

* wip(go): pass scoped ctx to siem grpc client;

* wip(go): remove unused publisher;

* wip(go): publish all tenant exporter updates on pull;

* wip(go): fix wg usage;

* wip(go): tenant scope the ctx when getting server info;

* wip(go): add tenant id to enrollment key token;

* wip(go): make host register tenant scope;

* wip(go): add default settings;

* wip(go): use correct checks for is msp tenant;

* wip(go): add email validation extension;

* wip(go): add pages for validation check;

* wip(go): use extension for validating user;

* wip(go): add invite type;

* wip(go): create only one validation invite per user email;

* wip(go): add validate email handler;

* wip(go): add email builder;

* wip(go): add user invite type migration logic;

* wip(go): only log email send failures;

* wip(go): add tenant id to flows table;

* wip(go): add func to check if connected to clickhouse;

* wip(go): migrate clickhouse;

* wip(go): add tenant id to mdm and edr state;

* wip(go): add delete methods for tables;

* wip(go): add tenant teardown method;

* wip(go): handle tenant deletion;

* wip(go): clean up flow logs on tenant deletion;

* wip(go): add update org membership role id method;

* wip(go): add transfer org ownership api;

* wip(go): move audit logs retention period to org settings;

* wip(go):get retention period from org settings;

* wip(go): remove old audit logs usage;

* wip(go): migrate audit logs retention period;

* wip(go): unmarshal decodes nums as float64;

* wip(go): run flow logs cleanup for all tenants in one go;

* wip(go): add audit logs retention period and smtp settings to tenant settings table for non-msp tenants;

* wip(go): resolve retention period and smtp settings correctly;

* wip(go): handle retention period and smtp settings updates and fetches correctly;

* wip(go): overlay retention period and smtp settings on server settings;;

* wip(go): upsert overlayed settings;

* wip(go): add azure tenant id to org settings;

* wip(go): tenant scope list roles;

* wip(go): add noop creds;

* wip(go): use first instead of find;

* wip(go): use correct roles when transfering ownership;

* wip(go): delete access tokens only if tenant scoped;

* wip(go): make delete user work for both org and tenant scope;

* wip(go): delete extclients only if tenant scoped;

* wip(go): check if caller is allowed to delete user;

* wip(go): infer scope for update user;

* wip(go): prevent org-owner elevation;

* wip(go): permissions check for update;

* wip(go): delete user access tokens only if tenant scope;

* wip(go): delete extclients only if tenant scope;

* wip(go): move upsert membership in upsert user;

* wip(go): set api host when constructing server config;

* wip(go): fix static errors;

* wip(go): fix test cases;

* wip(go): use correct get ns order;

* wip(go): use correct get pending host order;

* wip(go): use correct get access token order;

* wip(go): use correct get event order;

* wip(go): return error response on emqx user creation error;

* wip(go): store correct value in cache;

* wip(go): return better error response;

* wip(go): return better error response;

* wip(go): use cache when listing egress acls;

* wip(go): wire ctx in UserHasNetworkGroupAccess;

* wip(go): remove double scope wrapping;

* wip(go): add security check for list and get org;

* wip(go): ratelimit discover api;

* wip(go): expect sole org id in get and create org owner;

* wip(go): wrap create org owner in tx;

* wip(go): wire ctx correctly;

* wip(go): wire ctx for posture check functions;

* wip(go): remove resolved todo;

* wip(go): wire ctx for posture check function;

* wip(go): remove scope from netdesk login api;

* wip(go): use orgid correctly;

* wip(go): scope user update checks correctly;

* wip(go): wire ctx for fetch oauth secret;

* wip(go): add tenant_id and org_id query string param to user invite url;

* wip(go): get sole org;

* wip(go): set query string correctly;

* wip(go): wire ctx in update user groups;

* wip(go): allow delete oauth non-idp users;

* wip(go): delete access tokens post update but check before update;

* wip(go): ignore h.Nodes on create host;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): create tenant membership for org user directly;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): get user with membership on callback;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): use write locks;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): prevent create, update, delete of roles;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): ensure role belongs to network we are checking permission for;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): handle error on invite creation correctly;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): handle invite not found on org scoped oauth callback;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): init default groups on tenant creation;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): upset org and tenant on change;

* wip(go): use GetPlatformRole method to get platform role;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): created default network roles on tenant creation;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): use GetAnyRole when unsure; remove unused funcs;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): initialize network roles on startup;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): use GetNetworkRole method to get network role;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): migrate ids of all network roles;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): check both scoped and unscoped on delete;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): check both scoped and unscoped on update;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): scope id on network role upsert;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): add methods to fetch platform and network roles;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): scope network role id on create;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): add methods to scope and unscope role ids;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): remove uuid check;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): list scoped network roles;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): remove unuse update method;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): use correct id on get and delete;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): allow all_networks role in permission access;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): scope ctx from state cache;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): remove create, delete and update roles from netmaker cli;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): create network admin and user with ctx;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): allow updating names and metadata;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): remove tenant-scoping from headless sso apis;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): check for status code before unmarshalling;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): allow access to discover api on license validation error;

Signed-off-by: VishalDalwadi <[email protected]>

* wip(go): use tx;

Signed-off-by: VishalDalwadi <[email protected]>

---------

Signed-off-by: VishalDalwadi <[email protected]>
2026-08-18 16:22:10 +05:30

316 lines
9.0 KiB
Go

package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/orchestrator"
proLogic "github.com/gravitl/netmaker/pro/logic"
"github.com/gravitl/netmaker/schema"
"github.com/gravitl/netmaker/scope"
"github.com/gravitl/netmaker/servercfg"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
"gorm.io/gorm"
)
// GitHubProvider implements Provider for GitHub OAuth2.
type GitHubProvider struct {
cfg *oauth2.Config
}
// NewGitHubProvider constructs a GitHubProvider for the given OAuth2 credentials.
func NewGitHubProvider(redirectURL, clientID, clientSecret string) *GitHubProvider {
return &GitHubProvider{
cfg: &oauth2.Config{
RedirectURL: redirectURL,
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{"read:user", "user:email"},
Endpoint: github.Endpoint,
},
}
}
func (p *GitHubProvider) Name() string {
return github_provider_name
}
func (p *GitHubProvider) Config() *oauth2.Config {
return p.cfg
}
func (p *GitHubProvider) HandleLogin(w http.ResponseWriter, r *http.Request) {
appName := r.Header.Get("X-Application-Name")
if appName == "" {
appName = logic.NetmakerDesktopApp
}
oauthState := logic.RandomString(user_signin_length)
err := logic.SetState(scope.Level(r.Context()), scope.ID(r.Context()), appName, oauthState)
if err != nil {
handleSomethingWentWrong(w)
return
}
url := p.cfg.AuthCodeURL(oauthState)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func (p *GitHubProvider) HandleCallback(w http.ResponseWriter, r *http.Request) {
var rState, rCode = getStateAndCode(r)
state, err := logic.GetState(rState)
if err != nil {
handleOauthNotValid(w)
return
}
content, err := p.GetUserInfo(rState, rCode)
if err != nil {
logger.Log(1, "error when getting user info from github:", err.Error())
if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
handleOauthNotValid(w)
return
}
handleOauthNotConfigured(w)
return
}
var inviteExists bool
in, err := logic.GetUserInvite(r.Context(), content.Email)
if err == nil {
inviteExists = true
}
if !inviteExists && logic.IsPendingUser(r.Context(), content.Email) {
handleOauthUserSignUpApprovalPending(w)
return
}
// if user exists with provider login ID, migrate them to email-based username
user := &schema.User{Username: content.Login}
err = user.GetWithMembership(r.Context())
if err == nil {
if user.AuthType == schema.BasicAuth {
logger.Log(0, "invalid auth type: basic_auth")
handleAuthTypeMismatch(w)
return
}
emailCheck := &schema.User{Username: content.Email}
err = emailCheck.Get(r.Context())
if err != nil {
user.Username = content.Email
user.ExternalIdentityProviderID = content.Login
user.EmailValidated = true
_ = user.Update(r.Context())
}
}
emailCheck := &schema.User{Username: content.Email}
err = emailCheck.GetWithMembership(r.Context())
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if inviteExists {
user, err := proLogic.PrepareOauthUserFromInvite(r.Context(), in)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
user.ExternalIdentityProviderID = string(content.ID)
err = orchestrator.GetRepository().UserOrchestrator().CreateUser(r.Context(), &user)
if err != nil {
if errors.Is(err, logic.ErrUserLimitExceeded) {
handleUserLimitExceeded(w)
return
}
handleSomethingWentWrong(w)
return
}
_ = (&schema.UserInvite{
Email: content.Email,
}).DeleteByEmail(r.Context())
_ = (&schema.PendingUser{
Username: content.Email,
}).Delete(r.Context())
} else {
if scope.Level(r.Context()) == scope.OrgScope {
handleOauthInviteNotFound(w)
return
}
if !isEmailAllowed(r.Context(), content.Email) {
handleOauthUserNotAllowedToSignUp(w)
return
}
pendingUser := &schema.PendingUser{
Scope: scope.Level(r.Context()),
ScopeID: scope.ID(r.Context()),
Username: content.Email,
ExternalIdentityProviderID: string(content.ID),
}
err = pendingUser.Create(r.Context())
if err != nil {
handleSomethingWentWrong(w)
return
}
handleFirstTimeOauthUserSignUp(w)
return
}
} else {
handleSomethingWentWrong(w)
return
}
}
user = &schema.User{Username: content.Email}
err = user.GetWithMembership(r.Context())
if err != nil {
handleOauthUserNotFound(w)
return
}
if user.AccountDisabled {
handleUserAccountDisabled(w)
return
}
userRole := &schema.UserRole{ID: user.PlatformRoleID}
err = userRole.GetPlatformRole(r.Context())
if err != nil {
handleSomethingWentWrong(w)
return
}
if userRole.DenyDashboardAccess {
handleOauthUserNotAllowed(w)
return
}
newPass, fetchErr := logic.FetchOAuthSecret(r.Context())
if fetchErr != nil {
return
}
authRequest := models.UserAuthParams{
UserName: content.Email,
Password: newPass,
}
jwt, jwtErr := logic.VerifyAuthRequest(r.Context(), authRequest, state.AppName)
if jwtErr != nil {
logger.Log(1, "could not parse jwt for user", authRequest.UserName)
return
}
logic.LogEvent(r.Context(), &models.Event{
Action: schema.Login,
Source: models.Subject{
ID: user.Username,
Name: user.Username,
Type: schema.UserSub,
},
TriggeredBy: user.Username,
Target: models.Subject{
ID: schema.DashboardSub.String(),
Name: schema.DashboardSub.String(),
Type: schema.DashboardSub,
Info: logic.ToReturnUser(user),
},
Origin: schema.Dashboard,
})
logger.Log(1, "completed github OAuth signin for", content.Email)
http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
}
func (p *GitHubProvider) GetUserInfo(state, code string) (*OAuthUser, error) {
oauth_state_string, isValid := logic.IsStateValid(state)
if (!isValid || state != oauth_state_string) && !isStateCached(state) {
return nil, fmt.Errorf("invalid oauth state")
}
token, err := p.cfg.Exchange(context.Background(), code, oauth2.SetAuthURLParam("prompt", "login"))
if err != nil {
return nil, fmt.Errorf("code exchange failed: %s", err.Error())
}
if !token.Valid() {
return nil, fmt.Errorf("GitHub code exchange yielded invalid token")
}
data, err := json.Marshal(token)
if err != nil {
return nil, fmt.Errorf("failed to convert token to json: %s", err.Error())
}
httpReq, reqErr := http.NewRequest("GET", "https://api.github.com/user", nil)
if reqErr != nil {
return nil, fmt.Errorf("failed to create request to GitHub")
}
httpReq.Header.Set("Authorization", "token "+token.AccessToken)
response, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed getting user info: %s", err.Error())
}
defer response.Body.Close()
contents, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed reading response body: %s", err.Error())
}
userInfo := &OAuthUser{}
if err = json.Unmarshal(contents, userInfo); err != nil {
return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error())
}
userInfo.AccessToken = string(data)
if userInfo.Email == "" {
logger.Log(2, "fetching user email from github api")
userInfo.Email, err = getGithubEmailsInfo(token.AccessToken)
if err != nil {
logger.Log(0, "failed to fetch user's email from github: ", err.Error())
}
}
if userInfo.Email == "" {
return userInfo, errors.New("failed to fetch user email from SSO state")
}
return userInfo, nil
}
func getGithubEmailsInfo(accessToken string) (string, error) {
httpReq, reqErr := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
if reqErr != nil {
return "", fmt.Errorf("failed to create request to GitHub")
}
httpReq.Header.Add("Accept", "application/vnd.github.v3+json")
httpReq.Header.Set("Authorization", "token "+accessToken)
response, err := http.DefaultClient.Do(httpReq)
if err != nil {
return "", fmt.Errorf("failed getting user info: %s", err.Error())
}
defer response.Body.Close()
contents, err := io.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("failed reading response body: %s", err.Error())
}
if response.StatusCode != http.StatusOK {
var errInfo struct {
Message string `json:"message"`
}
if jsonErr := json.Unmarshal(contents, &errInfo); jsonErr == nil && errInfo.Message != "" {
return "", fmt.Errorf("failed getting user emails from GitHub: %s", errInfo.Message)
}
return "", fmt.Errorf("failed getting user emails from GitHub: status %d", response.StatusCode)
}
var emailsInfo []interface{}
if err = json.Unmarshal(contents, &emailsInfo); err != nil {
return "", err
}
for _, info := range emailsInfo {
emailInfoMap := info.(map[string]interface{})
if emailInfoMap["primary"].(bool) {
return emailInfoMap["email"].(string), nil
}
}
return "", errors.New("email not found")
}