Files
netmaker/pro/logic/security.go
T
Abhishek Kondur edda2868fc NM-163: Users, Groups, Roles, Networks and Hosts Table Migration (#3910)
* feat(go): add user schema;

* feat(go): migrate to user schema;

* feat(go): add audit fields;

* feat(go): remove unused fields from the network model;

* feat(go): add network schema;

* feat(go): migrate to network schema;

* refactor(go): add comment to clarify migration logic;

* fix(go): test failures;

* fix(go): test failures;

* feat(go): change membership table to store memberships at all scopes;

* feat(go): add schema for access grants;

* feat(go): remove nameservers from new networks table; ensure db passed for schema functions;

* feat(go): set max conns for sqlite to 1;

* fix(go): issues updating user account status;

* refactor(go): remove converters and access grants;

* refactor(go): add json tags in schema models;

* refactor(go): rename file to migrate_v1_6_0.go;

* refactor(go): add user groups and user roles tables; use schema tables;

* refactor(go): inline get and list from schema package;

* refactor(go): inline get network and list users from schema package;

* fix(go): staticcheck issues;

* fix(go): remove test not in use; fix test case;

* fix(go): validate network;

* fix(go): resolve static checks;

* fix(go): new models errors;

* fix(go): test errors;

* fix(go): handle no records;

* fix(go): add validations for user object;

* fix(go): set correct extclient status;

* fix(go): test error;

* feat(go): make schema the base package;

* feat(go): add host schema;

* feat(go): use schema host everywhere;

* feat(go): inline get host, list hosts and delete host;

* feat(go): use non-ptr value;

* feat(go): use save to upsert all fields;

* feat(go): use save to upsert all fields;

* feat(go): save turn endpoint as string;

* feat(go): check for gorm error record not found;

* fix(go): test failures;

* fix(go): update all network fields;

* fix(go): update all network fields;

* feat(go): add paginated list networks api;

* feat(go): add paginated list users api;

* feat(go): add paginated list hosts api;

* feat(go): add pagination to list groups api;

* fix(go): comment;

* fix(go): implement marshal and unmarshal text for custom types;

* fix(go): implement marshal and unmarshal json for custom types;

* fix(go): just use the old model for unmarshalling;

* fix(go): implement marshal and unmarshal json for custom types;

* feat(go): remove paginated list networks api;

* feat(go): use custom paginated response object;

* fix(go): ensure default values for page and per_page are used when not passed;

* fix(go): rename v1.6.0 to v1.5.1;

* fix(go): check for gorm.ErrRecordNotFound instead of database.IsEmptyRecord;

* fix(go): use host id, not pending host id;

* feat(go): add filters to paginated apis;

* feat(go): add filters to paginated apis;

* feat(go): remove check for max username length;

* feat(go): add filters to count as well;

* feat(go): use library to check email address validity;

* feat(go): ignore pagination if params not passed;

* fix(go): pagination issues;

* fix(go): check exists before using;

* fix(go): remove debug log;

* fix(go): use gorm err record not found;

* fix(go): use gorm err record not found;

* fix(go): use user principal name when creating pending user;

* fix(go): use schema package for consts;

* fix(go): prevent disabling superadmin user;

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* fix(go): swap is admin and is superadmin;

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* fix(go): remove dead code block;

https://github.com/gravitl/netmaker/pull/3910#discussion_r2928837937

* fix(go): incorrect message when trying to disable self;

https://github.com/gravitl/netmaker/pull/3910#discussion_r2928837934

* fix(go): use correct header;

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* fix(go): return after error response;

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* fix(go): use correct order of params;

https://github.com/gravitl/netmaker/pull/3910#discussion_r2929593036

* fix(go): set default values for page and page size; use v2 instead of /list;

* Update logic/auth.go

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* Update schema/user_roles.go

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* fix(go): syntax error;

* fix(go): set default values when page and per_page are not passed or 0;

* fix(go): use uuid.parse instead of uuid.must parse;

* fix(go): review errors;

* fix(go): review errors;

* Update controllers/user.go

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* Update controllers/user.go

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* NM-163: fix errors:

* Update db/types/options.go

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* fix(go): persist return user in event;

* Update db/types/options.go

Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>

* NM-163: duplicate lines of code

* NM-163: fix(go): fix missing return and filter parsing in user controller

- Add missing return after error response in updateUserAccountStatus
  to prevent double-response and spurious ext-client side-effects
- Use switch statements in listUsers to skip unrecognized
  account_status and mfa_status filter values

* fix(go): check for both min and max page size;

* fix(go): enclose transfer superadmin in transaction;

* fix(go): review errors;

* fix(go): remove free tier checks;

* fix(go): review fixes;

---------

Co-authored-by: VishalDalwadi <dalwadivishal26@gmail.com>
Co-authored-by: Vishal Dalwadi <51291657+VishalDalwadi@users.noreply.github.com>
Co-authored-by: tenki-reviewer[bot] <262613592+tenki-reviewer[bot]@users.noreply.github.com>
2026-03-17 19:36:52 +05:30

293 lines
8.5 KiB
Go

package logic
import (
"errors"
"fmt"
"net/http"
"strings"
"context"
"github.com/gorilla/mux"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/schema"
"github.com/gravitl/netmaker/servercfg"
)
// constants for accounts api hosts
const (
// accountsHostDevelopment is the accounts api host for development environment
accountsHostDevelopment = "https://api.dev.accounts.netmaker.io"
// accountsHostStaging is the accounts api host for staging environment
accountsHostStaging = "https://api.staging.accounts.netmaker.io"
// accountsHostProduction is the accounts api host for production environment
accountsHostProduction = "https://api.accounts.netmaker.io"
)
// constants for accounts UI hosts
const (
// accountsUIHostDevelopment is the accounts UI host for development environment
accountsUIHostDevelopment = "https://account.dev.netmaker.io"
// accountsUIHostStaging is the accounts UI host for staging environment
accountsUIHostStaging = "https://account.staging.netmaker.io"
// accountsUIHostProduction is the accounts UI host for production environment
accountsUIHostProduction = "https://account.netmaker.io"
// saasNMUIHostDevelopment is the SaaS NMUI host for development environment
saasNMUIHostDevelopment = "https://app.dev.netmaker.io"
// saasNMUIHostStaging is the SaaS NMUI host for staging environment
saasNMUIHostStaging = "https://app.staging.netmaker.io"
// saasNMUIHostProduction is the SaaS NMUI host for production environment
saasNMUIHostProduction = "https://app.netmaker.io"
)
func NetworkPermissionsCheck(username string, r *http.Request) error {
// at this point global checks should be completed
user := &schema.User{Username: username}
err := user.Get(r.Context())
if err != nil {
return err
}
userRole := &schema.UserRole{ID: user.PlatformRoleID}
err = userRole.Get(r.Context())
if err != nil {
return errors.New("access denied")
}
if userRole.FullAccess {
return nil
}
if userRole.ID == schema.Auditor {
if r.Method == http.MethodGet {
return nil
} else {
return errors.New("access denied")
}
}
// get info from header to determine the target rsrc
targetRsrc := r.Header.Get("TARGET_RSRC")
targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
netID := r.Header.Get("NET_ID")
if targetRsrc == "" {
return errors.New("target rsrc is missing")
}
if r.Header.Get("RAC") == "true" && r.Method == http.MethodGet {
return nil
}
if netID == "" {
return errors.New("network id is missing")
}
if r.Method == "" {
r.Method = http.MethodGet
}
if targetRsrc == schema.MetricRsrc.String() {
return nil
}
for groupID := range user.UserGroups.Data() {
userG, err := GetUserGroup(groupID)
if err == nil {
if netRoles, ok := userG.NetworkRoles.Data()[schema.AllNetworks]; ok {
for netRoleID := range netRoles {
err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
if err == nil {
return nil
}
}
}
netRoles := userG.NetworkRoles.Data()[schema.NetworkID(netID)]
for netRoleID := range netRoles {
err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
if err == nil {
return nil
}
}
}
}
return errors.New("access denied")
}
func checkNetworkAccessPermissions(netRoleID schema.UserRoleID, username, reqScope, targetRsrc, targetRsrcID, netID string) error {
networkPermissionScope := &schema.UserRole{ID: netRoleID}
err := networkPermissionScope.Get(db.WithContext(context.TODO()))
if err != nil {
return err
}
if networkPermissionScope.FullAccess {
return nil
}
rsrcPermissionScope, ok := networkPermissionScope.NetworkLevelAccess.Data()[schema.RsrcType(targetRsrc)]
if !ok {
return errors.New("access denied")
}
if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[logic.GetAllRsrcIDForRsrc(schema.RsrcType(targetRsrc))]; ok {
// handle extclient apis here
if schema.RsrcType(targetRsrc) == schema.ExtClientsRsrc && allRsrcsTypePermissionScope.SelfOnly && targetRsrcID != "" {
extclient, err := logic.GetExtClient(targetRsrcID, netID)
if err != nil {
return err
}
if !logic.IsUserAllowedAccessToExtClient(username, extclient) {
return errors.New("access denied")
}
}
err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope)
if err == nil {
return nil
}
}
if targetRsrcID == "" {
return errors.New("target rsrc id is empty")
}
if scope, ok := rsrcPermissionScope[schema.RsrcID(targetRsrcID)]; ok {
err = checkPermissionScopeWithReqMethod(scope, reqScope)
if err == nil {
return nil
}
}
return errors.New("access denied")
}
func GlobalPermissionsCheck(username string, r *http.Request) error {
route, err := mux.CurrentRoute(r).GetPathTemplate()
if err != nil {
return err
}
user := &schema.User{Username: username}
err = user.Get(r.Context())
if err != nil {
return err
}
userRole := &schema.UserRole{ID: user.PlatformRoleID}
err = userRole.Get(r.Context())
if err != nil {
return errors.New("access denied")
}
if userRole.FullAccess {
return nil
}
if userRole.ID == schema.Auditor {
if r.Method == http.MethodGet {
return nil
} else {
if (r.Method == http.MethodPut || r.Method == http.MethodPost) &&
strings.Contains(r.URL.Path, "/api/users/"+username) {
return nil
}
return errors.New("access denied")
}
}
targetRsrc := r.Header.Get("TARGET_RSRC")
targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
if targetRsrc == "" {
return errors.New("target rsrc is missing")
}
if r.Method == "" {
r.Method = http.MethodGet
}
if targetRsrc == schema.MetricRsrc.String() {
return nil
}
if (targetRsrc == schema.HostRsrc.String() || targetRsrc == schema.NetworkRsrc.String()) && r.Method == http.MethodGet && targetRsrcID == "" {
return nil
}
if targetRsrc == schema.UserRsrc.String() && user.PlatformRoleID == schema.PlatformUser && r.Method == http.MethodPut &&
route == "/api/v1/users/add_network_user" || route == "/api/v1/users/remove_network_user" {
return nil
}
if targetRsrc == schema.UserRsrc.String() && user.PlatformRoleID == schema.PlatformUser && r.Method == http.MethodGet &&
route == "/api/v1/users/unassigned_network_users" {
return nil
}
if targetRsrc == schema.JitUserRsrc.String() && r.Method == http.MethodGet &&
strings.Contains(r.URL.Path, "/api/v1/jit_user/networks") {
return nil
}
if targetRsrc == schema.UserRsrc.String() && username == targetRsrcID && (r.Method != http.MethodDelete) {
return nil
}
if r.Method == http.MethodGet && targetRsrc == schema.UserActivityRsrc.String() && route == "/api/v1/user/activity" {
return nil
}
if r.Method == http.MethodGet && user.PlatformRoleID == schema.PlatformUser && (route == "/api/v1/network/activity" || route == "/api/v1/flows") {
return nil
}
rsrcPermissionScope, ok := userRole.GlobalLevelAccess.Data()[schema.RsrcType(targetRsrc)]
if !ok {
return fmt.Errorf("access denied to %s", targetRsrc)
}
if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[schema.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok {
return checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, r.Method)
}
if targetRsrcID == "" {
return errors.New("target rsrc id is missing")
}
if scope, ok := rsrcPermissionScope[schema.RsrcID(targetRsrcID)]; ok {
return checkPermissionScopeWithReqMethod(scope, r.Method)
}
return errors.New("access denied")
}
func checkPermissionScopeWithReqMethod(scope schema.RsrcPermissionScope, reqmethod string) error {
if reqmethod == http.MethodGet && scope.Read {
return nil
}
if (reqmethod == http.MethodPatch || reqmethod == http.MethodPut) && scope.Update {
return nil
}
if reqmethod == http.MethodDelete && scope.Delete {
return nil
}
if reqmethod == http.MethodPost && scope.Create {
return nil
}
return errors.New("operation not permitted")
}
func GetAccountsHost() string {
switch servercfg.GetEnvironment() {
case "dev":
return accountsHostDevelopment
case "staging":
return accountsHostStaging
default:
return accountsHostProduction
}
}
func GetAccountsUIHost() string {
switch servercfg.GetEnvironment() {
case "dev":
return accountsUIHostDevelopment
case "staging":
return accountsUIHostStaging
default:
return accountsUIHostProduction
}
}
func GetSaaSNMUIHost() string {
switch servercfg.GetEnvironment() {
case "dev":
return saasNMUIHostDevelopment
case "staging":
return saasNMUIHostStaging
default:
return saasNMUIHostProduction
}
}
func GetSaaSNMUIHostWithVersion() string {
return fmt.Sprintf("%s/%s", GetSaaSNMUIHost(), servercfg.GetVersion())
}