NET-1991: Add IDP sync functionality. (#3428)

* feat: api access tokens

* revoke all user tokens

* redefine access token api routes, add auto egress option to enrollment keys

* add server settings apis, add db table for settigs

* handle server settings updates

* switch to using settings from DB

* fix sever settings migration

* revet force migration for settings

* fix server settings database write

* fix revoked tokens to be unauthorized

* remove unused functions

* convert access token to sql schema

* switch access token to sql schema

* fix merge conflicts

* fix server settings types

* bypass basic auth setting for super admin

* add TODO comment

* feat(go): add types for idp package;

* feat(go): import azure sdk;

* feat(go): add stub for google workspace client;

* feat(go): implement azure ad client;

* feat(go): sync users and groups using idp client;

* publish peer update on settings update

* feat(go): read creds from env vars;

* feat(go): add api endpoint to trigger idp sync;

* fix(go): sync member changes;

* fix(go): handle error;

* fix(go): set correct response type;

* feat(go): support disabling user accounts;

1. Add api endpoints to enable and disable user accounts.
2. Add checks in authenticators to prevent disabled users from logging in.
3. Add checks in middleware to prevent api usage by disabled users.

* feat(go): use string slice for group members;

* feat(go): sync user account status from idp;

* feat(go): import google admin sdk;

* feat(go): add support for google workspace idp;

* feat(go): initialize idp client on sync;

* feat(go): sync from idp periodically;

* feat(go): improvements for google idp;

1. Use the impersonate package to authenticate.
2. Use Pages method to get all data.

* chore(go): import style changes from migration branch;

1. Singular file names for table schema.
2. No table name method.
3. Use .Model instead of .Table.
4. No unnecessary tagging.

* remove nat check on egress gateway request

* Revert "remove nat check on egress gateway request"

This reverts commit 0aff12a189.

* feat(go): add db middleware;

* feat(go): restore method;

* feat(go): add user access token schema;

* fix user auth api:

* re initalise oauth and email config

* feat(go): fetch idp creds from server settings;

* feat(go): add filters for users and groups;

* feat(go): skip sync from idp if disabled;

* feat(go): add endpoint to remove idp integration;

* feat(go): import all users if no filters;

* feat(go): assign service-user role on sync;

* feat(go): remove microsoft-go-sdk;

* feat(go): add display name field for user;

* fix(go): set account disabled correctly;

* fix(go): update user if display name changes;

* fix(go): remove auth provider when removing idp integration;

* fix(go): ignore display name if empty;

* feat(go): add idp sync interval setting;

* fix(go): error on invalid auth provider;

* fix(go): no error if no user on group delete;

* fix(go): check superadmin using platform role id;

* feat(go): add display name and account disabled to return user as well;

* feat(go): tidy go mod after merge;

* feat(go): reinitialize auth provider and idp sync hook;

* fix(go): merge error;

* fix(go): merge error;

* feat(go): use id as the external provider id;

* fix(go): comments;

* feat(go): add function to return pending users;

* feat(go): prevent external id erasure;

* fix(go): user and group sync errors;

* chore(go): cleanup;

* fix(go): delete only oauth users;

* feat(go): use uuid group id;

* export ipd id to in rest api

* feat(go): don't use uuid for default groups;

* feat(go): migrate group only if id not uuid;

* chore(go): go mod tidy;

---------

Co-authored-by: abhishek9686 <abhi281342@gmail.com>
Co-authored-by: Abhishek K <abhishek@netmaker.io>
Co-authored-by: the_aceix <aceixsmartx@gmail.com>
This commit is contained in:
Vishal Dalwadi 2025-05-21 01:18:15 -07:00 committed by GitHub
parent d7bad9865a
commit 614cf77b5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1168 additions and 105 deletions

View file

@ -297,9 +297,10 @@ func updateSettings(w http.ResponseWriter, r *http.Request) {
func reInit(curr, new models.ServerSettings, force bool) {
logic.SettingsMutex.Lock()
defer logic.SettingsMutex.Unlock()
logic.InitializeAuthProvider()
logic.ResetAuthProvider()
logic.EmailInit()
logic.SetVerbosity(int(logic.GetServerSettings().Verbosity))
logic.ResetIDPSyncHook()
// check if auto update is changed
if force {
if curr.NetclientAutoUpdate != new.NetclientAutoUpdate {

View file

@ -37,6 +37,8 @@ func userHandlers(r *mux.Router) {
r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).Methods(http.MethodPost)
r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete)
r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
r.HandleFunc("/api/users/{username}/enable", logic.SecurityCheck(true, http.HandlerFunc(enableUserAccount))).Methods(http.MethodPost)
r.HandleFunc("/api/users/{username}/disable", logic.SecurityCheck(true, http.HandlerFunc(disableUserAccount))).Methods(http.MethodPost)
r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
@ -270,6 +272,13 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
return
}
if user.AccountDisabled {
err = errors.New("user account disabled")
logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
return
}
if !user.IsSuperAdmin && !logic.IsBasicAuthEnabled() {
logic.ReturnErrorResponse(
response,
@ -446,6 +455,65 @@ func getUser(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(user)
}
// @Summary Enable a user's account
// @Router /api/users/{username}/enable [post]
// @Tags Users
// @Param username path string true "Username of the user to enable"
// @Success 200 {object} models.SuccessResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func enableUserAccount(w http.ResponseWriter, r *http.Request) {
username := mux.Vars(r)["username"]
user, err := logic.GetUser(username)
if err != nil {
logger.Log(0, "failed to fetch user: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
user.AccountDisabled = false
err = logic.UpsertUser(*user)
if err != nil {
logger.Log(0, "failed to enable user account: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
}
logic.ReturnSuccessResponse(w, r, "user account enabled")
}
// @Summary Disable a user's account
// @Router /api/users/{username}/disable [post]
// @Tags Users
// @Param username path string true "Username of the user to disable"
// @Success 200 {object} models.SuccessResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func disableUserAccount(w http.ResponseWriter, r *http.Request) {
username := mux.Vars(r)["username"]
user, err := logic.GetUser(username)
if err != nil {
logger.Log(0, "failed to fetch user: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if user.PlatformRoleID == models.SuperAdminRole {
err = errors.New("cannot disable super-admin user account")
logger.Log(0, err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
user.AccountDisabled = true
err = logic.UpsertUser(*user)
if err != nil {
logger.Log(0, "failed to disable user account: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
}
logic.ReturnSuccessResponse(w, r, "user account disabled")
}
// swagger:route GET /api/v1/users user getUserV1
//
// Get an individual user with role info.

View file

@ -19,8 +19,6 @@ const (
DELETED_NODES_TABLE_NAME = "deletednodes"
// USERS_TABLE_NAME - users table
USERS_TABLE_NAME = "users"
// ACCESS_TOKENS_TABLE_NAME - access tokens table
ACCESS_TOKENS_TABLE_NAME = "user_access_tokens"
// USER_PERMISSIONS_TABLE_NAME - user permissions table
USER_PERMISSIONS_TABLE_NAME = "user_permissions"
// CERTS_TABLE_NAME - certificates table

27
go.mod
View file

@ -6,7 +6,7 @@ toolchain go1.23.7
require (
github.com/blang/semver v3.5.1+incompatible
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/eclipse/paho.mqtt.golang v1.5.0
github.com/go-playground/validator/v10 v10.26.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
@ -21,7 +21,7 @@ require (
github.com/txn2/txeh v1.5.5
go.uber.org/automaxprocs v1.6.0
golang.org/x/crypto v0.38.0
golang.org/x/net v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.29.0
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
@ -42,11 +42,13 @@ require (
)
require (
github.com/google/go-cmp v0.7.0
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
github.com/guumaster/tablewriter v0.0.10
github.com/matryer/is v1.4.1
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.9.1
google.golang.org/api v0.229.0
gopkg.in/mail.v2 v2.3.1
gorm.io/datatypes v1.2.5
gorm.io/driver/postgres v1.5.11
@ -55,11 +57,17 @@ require (
)
require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/auth v0.16.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@ -68,18 +76,25 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/seancfoley/bintree v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/hashicorp/go-version v1.7.0

68
go.sum
View file

@ -1,5 +1,9 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
@ -9,18 +13,22 @@ github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szN
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -38,12 +46,18 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@ -72,8 +86,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@ -123,14 +137,30 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
@ -139,8 +169,20 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -8,15 +8,16 @@ import (
"fmt"
"time"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/schema"
"github.com/go-playground/validator/v10"
"golang.org/x/crypto/bcrypt"
"golang.org/x/exp/slog"
"github.com/gravitl/netmaker/database"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/schema"
)
const (
@ -31,7 +32,8 @@ func ClearSuperUserCache() {
superUser = models.User{}
}
var InitializeAuthProvider = func() string { return "" }
var ResetAuthProvider = func() {}
var ResetIDPSyncHook = func() {}
// HasSuperAdmin - checks if server has an superadmin/owner
func HasSuperAdmin() (bool, error) {
@ -303,11 +305,55 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
return userchange, errors.New("invalid network roles: " + err.Error())
}
if userchange.DisplayName != "" {
if user.ExternalIdentityProviderID != "" &&
user.DisplayName != userchange.DisplayName {
return userchange, errors.New("display name cannot be updated for external user")
}
user.DisplayName = userchange.DisplayName
}
if user.ExternalIdentityProviderID != "" &&
userchange.AccountDisabled != user.AccountDisabled {
return userchange, errors.New("account status cannot be updated for external user")
}
// Reset Gw Access for service users
go UpdateUserGwAccess(*user, *userchange)
if userchange.PlatformRoleID != "" {
user.PlatformRoleID = userchange.PlatformRoleID
}
for groupID := range userchange.UserGroups {
_, ok := user.UserGroups[groupID]
if !ok {
group, err := GetUserGroup(groupID)
if err != nil {
return userchange, err
}
if group.ExternalIdentityProviderID != "" {
return userchange, errors.New("cannot modify membership of external groups")
}
}
}
for groupID := range user.UserGroups {
_, ok := userchange.UserGroups[groupID]
if !ok {
group, err := GetUserGroup(groupID)
if err != nil {
return userchange, err
}
if group.ExternalIdentityProviderID != "" {
return userchange, errors.New("cannot modify membership of external groups")
}
}
}
user.UserGroups = userchange.UserGroups
user.NetworkRoles = userchange.NetworkRoles
AddGlobalNetRolesToAdmins(user)

View file

@ -163,9 +163,11 @@ func GetUserNameFromToken(authtoken string) (username string, err error) {
// VerifyUserToken func will used to Verify the JWT Token while using APIS
func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin bool, err error) {
claims := &models.UserClaims{}
if tokenString == servercfg.GetMasterKey() && servercfg.GetMasterKey() != "" {
return MasterUser, true, true, nil
}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtSecretKey, nil
})

View file

@ -1,6 +1,7 @@
package logic
import (
"errors"
"net/http"
"strings"
@ -32,6 +33,19 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
return
}
user, err := GetUser(username)
if err != nil {
ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
return
}
if user.AccountDisabled {
err = errors.New("user account disabled")
ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
return
}
// detect masteradmin
if username == MasterUser {
r.Header.Set("ismaster", "yes")

View file

@ -272,6 +272,26 @@ func GetAzureTenant() string {
return GetServerSettings().AzureTenant
}
// IsSyncEnabled returns whether auth provider sync is enabled.
func IsSyncEnabled() bool {
return GetServerSettings().SyncEnabled
}
// GetIDPSyncInterval returns the interval at which the netmaker should sync
// data from IDP.
func GetIDPSyncInterval() time.Duration {
syncInterval, err := time.ParseDuration(GetServerSettings().IDPSyncInterval)
if err != nil {
return 24 * time.Hour
}
if syncInterval == 0 {
return 24 * time.Hour
}
return syncInterval
}
// GetMetricsPort - get metrics port
func GetMetricsPort() int {
return GetServerSettings().MetricsPort

View file

@ -50,6 +50,8 @@ var MigrateUserRoleAndGroups = func(u models.User) {
}
var MigrateGroups = func() {}
var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }

View file

@ -41,13 +41,15 @@ func GetReturnUser(username string) (models.ReturnUser, error) {
// ToReturnUser - gets a user as a return user
func ToReturnUser(user models.User) models.ReturnUser {
return models.ReturnUser{
UserName: user.UserName,
PlatformRoleID: user.PlatformRoleID,
AuthType: user.AuthType,
UserGroups: user.UserGroups,
NetworkRoles: user.NetworkRoles,
RemoteGwIDs: user.RemoteGwIDs,
LastLoginTime: user.LastLoginTime,
UserName: user.UserName,
DisplayName: user.DisplayName,
AccountDisabled: user.AccountDisabled,
AuthType: user.AuthType,
RemoteGwIDs: user.RemoteGwIDs,
UserGroups: user.UserGroups,
PlatformRoleID: user.PlatformRoleID,
NetworkRoles: user.NetworkRoles,
LastLoginTime: user.LastLoginTime,
}
}
@ -78,7 +80,7 @@ func GetSuperAdmin() (models.ReturnUser, error) {
return models.ReturnUser{}, err
}
for _, user := range users {
if user.IsSuperAdmin {
if user.IsSuperAdmin || user.PlatformRoleID == models.SuperAdminRole {
return user, nil
}
}
@ -113,7 +115,7 @@ func IsPendingUser(username string) bool {
return false
}
func ListPendingUsers() ([]models.ReturnUser, error) {
func ListPendingReturnUsers() ([]models.ReturnUser, error) {
pendingUsers := []models.ReturnUser{}
records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
if err != nil && !database.IsEmptyRecord(err) {
@ -129,6 +131,22 @@ func ListPendingUsers() ([]models.ReturnUser, error) {
return pendingUsers, nil
}
func ListPendingUsers() ([]models.User, error) {
var pendingUsers []models.User
records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
if err != nil && !database.IsEmptyRecord(err) {
return pendingUsers, err
}
for _, record := range records {
var u models.User
err = json.Unmarshal([]byte(record), &u)
if err == nil {
pendingUsers = append(pendingUsers, u)
}
}
return pendingUsers, nil
}
func GetUserMap() (map[string]models.User, error) {
userMap := make(map[string]models.User)
records, err := database.FetchRecords(database.USERS_TABLE_NAME)

View file

@ -29,6 +29,7 @@ func Run() {
assignSuperAdmin()
createDefaultTagsAndPolicies()
removeOldUserGrps()
syncGroups()
syncUsers()
updateHosts()
updateNodes()
@ -393,6 +394,10 @@ func MigrateEmqx() {
}
func syncGroups() {
logic.MigrateGroups()
}
func syncUsers() {
// create default network user roles for existing networks
if servercfg.IsPro {

View file

@ -15,7 +15,13 @@ type ServerSettings struct {
OIDCIssuer string `json:"oidcissuer"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
SyncEnabled bool `json:"sync_enabled"`
GoogleAdminEmail string `json:"google_admin_email"`
GoogleSACredsJson string `json:"google_sa_creds_json"`
AzureTenant string `json:"azure_tenant"`
UserFilters []string `json:"user_filters"`
GroupFilters []string `json:"group_filters"`
IDPSyncInterval string `json:"idp_sync_interval"`
Telemetry string `json:"telemetry"`
BasicAuth bool `json:"basic_auth"`
JwtValidityDuration int `json:"jwt_validity_duration"`

View file

@ -144,17 +144,20 @@ type CreateGroupReq struct {
}
type UserGroup struct {
ID UserGroupID `json:"id"`
Default bool `json:"default"`
Name string `json:"name"`
NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
MetaData string `json:"meta_data"`
ID UserGroupID `json:"id"`
ExternalIdentityProviderID string `json:"external_identity_provider_id"`
Default bool `json:"default"`
Name string `json:"name"`
NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
MetaData string `json:"meta_data"`
}
// User struct - struct for Users
type User struct {
UserName string `json:"username" bson:"username" validate:"min=3,in_charset|email"`
ExternalIdentityProviderID string `json:"external_identity_provider_id"`
DisplayName string `json:"display_name"`
AccountDisabled bool `json:"account_disabled"`
Password string `json:"password" bson:"password" validate:"required,min=5"`
IsAdmin bool `json:"isadmin" bson:"isadmin"` // deprecated
IsSuperAdmin bool `json:"issuperadmin"` // deprecated
@ -174,15 +177,18 @@ type ReturnUserWithRolesAndGroups struct {
// ReturnUser - return user struct
type ReturnUser struct {
UserName string `json:"username"`
IsAdmin bool `json:"isadmin"`
IsSuperAdmin bool `json:"issuperadmin"`
AuthType AuthType `json:"auth_type"`
RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated
UserGroups map[UserGroupID]struct{} `json:"user_group_ids"`
PlatformRoleID UserRoleID `json:"platform_role_id"`
NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
LastLoginTime time.Time `json:"last_login_time"`
UserName string `json:"username"`
ExternalIdentityProviderID string `json:"external_identity_provider_id"`
DisplayName string `json:"display_name"`
AccountDisabled bool `json:"account_disabled"`
IsAdmin bool `json:"isadmin"`
IsSuperAdmin bool `json:"issuperadmin"`
AuthType AuthType `json:"auth_type"`
RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated
UserGroups map[UserGroupID]struct{} `json:"user_group_ids"`
PlatformRoleID UserRoleID `json:"platform_role_id"`
NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
LastLoginTime time.Time `json:"last_login_time"`
}
// UserAuthParams - user auth params struct

View file

@ -34,6 +34,7 @@ const (
// OAuthUser - generic OAuth strategy user
type OAuthUser struct {
ID string `json:"id" bson:"id"`
Name string `json:"name" bson:"name"`
Email string `json:"email" bson:"email"`
Login string `json:"login" bson:"login"`
@ -63,6 +64,17 @@ func getCurrentAuthFunctions() map[string]interface{} {
}
}
// ResetAuthProvider resets the auth provider configuration.
func ResetAuthProvider() {
settings := logic.GetServerSettings()
if settings.AuthProvider == "" {
auth_provider = nil
}
InitializeAuthProvider()
}
// InitializeAuthProvider - initializes the auth provider if any is present
func InitializeAuthProvider() string {
var functions = getCurrentAuthFunctions()

View file

@ -111,7 +111,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
user.ExternalIdentityProviderID = content.UserPrincipalName
user.ExternalIdentityProviderID = content.ID
if err = logic.CreateUser(&user); err != nil {
handleSomethingWentWrong(w)
return
@ -124,7 +124,9 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
return
}
err = logic.InsertPendingUser(&models.User{
UserName: content.Email,
UserName: content.Email,
ExternalIdentityProviderID: content.ID,
AuthType: models.OAuth,
})
if err != nil {
handleSomethingWentWrong(w)
@ -152,6 +154,12 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
handleOauthUserNotFound(w)
return
}
if user.AccountDisabled {
handleUserAccountDisabled(w)
return
}
userRole, err := logic.GetRole(user.PlatformRoleID)
if err != nil {
handleSomethingWentWrong(w)

View file

@ -113,6 +113,8 @@ var notallowedtosignup = fmt.Sprintf(htmlBaseTemplate, `<h2>Your email is not al
var authTypeMismatch = fmt.Sprintf(htmlBaseTemplate, `<h2>It looks like you already have an account with us using Basic Authentication.</h2>
<p>To continue, please log in with your existing credentials or reset your password if needed.</p>`)
var userAccountDisabled = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account has been disabled. Please contact your administrator for more information about your account.</h2>`)
func handleOauthUserNotFound(response http.ResponseWriter) {
response.Header().Set("Content-Type", "text/html; charset=utf-8")
response.WriteHeader(http.StatusNotFound)
@ -166,3 +168,9 @@ func handleAuthTypeMismatch(response http.ResponseWriter) {
response.WriteHeader(http.StatusBadRequest)
response.Write([]byte(authTypeMismatch))
}
func handleUserAccountDisabled(response http.ResponseWriter) {
response.Header().Set("Content-Type", "text/html; charset=utf-8")
response.WriteHeader(http.StatusUnauthorized)
response.Write([]byte(userAccountDisabled))
}

View file

@ -111,7 +111,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
user.ExternalIdentityProviderID = content.Login
user.ExternalIdentityProviderID = content.ID
if err = logic.CreateUser(&user); err != nil {
handleSomethingWentWrong(w)
return
@ -124,7 +124,9 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
return
}
err = logic.InsertPendingUser(&models.User{
UserName: content.Email,
UserName: content.Email,
ExternalIdentityProviderID: content.ID,
AuthType: models.OAuth,
})
if err != nil {
handleSomethingWentWrong(w)
@ -143,6 +145,12 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
handleOauthUserNotFound(w)
return
}
if user.AccountDisabled {
handleUserAccountDisabled(w)
return
}
userRole, err := logic.GetRole(user.PlatformRoleID)
if err != nil {
handleSomethingWentWrong(w)

View file

@ -105,7 +105,9 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
return
}
err = logic.InsertPendingUser(&models.User{
UserName: content.Email,
UserName: content.Email,
ExternalIdentityProviderID: content.ID,
AuthType: models.OAuth,
})
if err != nil {
handleSomethingWentWrong(w)
@ -136,6 +138,11 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
return
}
if user.AccountDisabled {
handleUserAccountDisabled(w)
return
}
userRole, err := logic.GetRole(user.PlatformRoleID)
if err != nil {
handleSomethingWentWrong(w)

View file

@ -64,7 +64,9 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
if err != nil {
if database.IsEmptyRecord(err) { // user must not exist, so try to make one
err = logic.InsertPendingUser(&models.User{
UserName: userClaims.getUserName(),
UserName: userClaims.getUserName(),
ExternalIdentityProviderID: userClaims.ID,
AuthType: models.OAuth,
})
if err != nil {
handleSomethingWentWrong(w)

View file

@ -102,7 +102,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
user.ExternalIdentityProviderID = content.Email
user.ExternalIdentityProviderID = content.ID
if err = logic.CreateUser(&user); err != nil {
handleSomethingWentWrong(w)
return
@ -115,7 +115,9 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
return
}
err = logic.InsertPendingUser(&models.User{
UserName: content.Email,
UserName: content.Email,
ExternalIdentityProviderID: content.ID,
AuthType: models.OAuth,
})
if err != nil {
handleSomethingWentWrong(w)
@ -143,6 +145,12 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
handleOauthUserNotFound(w)
return
}
if user.AccountDisabled {
handleUserAccountDisabled(w)
return
}
userRole, err := logic.GetRole(user.PlatformRoleID)
if err != nil {
handleSomethingWentWrong(w)
@ -224,6 +232,8 @@ func getOIDCUserInfo(state string, code string) (u *OAuthUser, e error) {
e = fmt.Errorf("error when claiming OIDCUser: \"%s\"", err.Error())
}
u.ID = idToken.Subject
return
}

281
pro/auth/sync.go Normal file
View file

@ -0,0 +1,281 @@
package auth
import (
"fmt"
"github.com/gravitl/netmaker/database"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/pro/idp"
"github.com/gravitl/netmaker/pro/idp/azure"
"github.com/gravitl/netmaker/pro/idp/google"
proLogic "github.com/gravitl/netmaker/pro/logic"
"strings"
"time"
)
var syncTicker *time.Ticker
func StartSyncHook() {
syncTicker = time.NewTicker(logic.GetIDPSyncInterval())
for range syncTicker.C {
err := SyncFromIDP()
if err != nil {
logger.Log(0, "failed to sync from idp: ", err.Error())
} else {
logger.Log(0, "sync from idp complete")
}
}
}
func ResetIDPSyncHook() {
if syncTicker != nil {
syncTicker.Stop()
if logic.IsSyncEnabled() {
go StartSyncHook()
}
}
}
func SyncFromIDP() error {
settings := logic.GetServerSettings()
var idpClient idp.Client
var idpUsers []idp.User
var idpGroups []idp.Group
var err error
switch settings.AuthProvider {
case "google":
idpClient, err = google.NewGoogleWorkspaceClient()
if err != nil {
return err
}
case "azure-ad":
idpClient = azure.NewAzureEntraIDClient()
default:
if settings.AuthProvider != "" {
return fmt.Errorf("invalid auth provider: %s", settings.AuthProvider)
}
}
if settings.AuthProvider != "" && idpClient != nil {
idpUsers, err = idpClient.GetUsers()
if err != nil {
return err
}
idpGroups, err = idpClient.GetGroups()
if err != nil {
return err
}
}
err = syncUsers(idpUsers)
if err != nil {
return err
}
return syncGroups(idpGroups)
}
func syncUsers(idpUsers []idp.User) error {
dbUsers, err := logic.GetUsersDB()
if err != nil && !database.IsEmptyRecord(err) {
return err
}
password, err := logic.FetchPassValue("")
if err != nil {
return err
}
idpUsersMap := make(map[string]struct{})
for _, user := range idpUsers {
idpUsersMap[user.Username] = struct{}{}
}
dbUsersMap := make(map[string]models.User)
for _, user := range dbUsers {
dbUsersMap[user.UserName] = user
}
filters := logic.GetServerSettings().UserFilters
for _, user := range idpUsers {
var found bool
for _, filter := range filters {
if strings.HasPrefix(user.Username, filter) {
found = true
break
}
}
// if there are filters but none of them match, then skip this user.
if len(filters) > 0 && !found {
continue
}
dbUser, ok := dbUsersMap[user.Username]
if !ok {
// create the user only if it doesn't exist.
err = logic.CreateUser(&models.User{
UserName: user.Username,
ExternalIdentityProviderID: user.ID,
DisplayName: user.DisplayName,
AccountDisabled: user.AccountDisabled,
Password: password,
AuthType: models.OAuth,
PlatformRoleID: models.ServiceUser,
})
if err != nil {
return err
}
} else if dbUser.AuthType == models.OAuth {
if dbUser.AccountDisabled != user.AccountDisabled ||
dbUser.DisplayName != user.DisplayName ||
dbUser.ExternalIdentityProviderID != user.ID {
dbUser.AccountDisabled = user.AccountDisabled
dbUser.DisplayName = user.DisplayName
dbUser.ExternalIdentityProviderID = user.ID
err = logic.UpsertUser(dbUser)
if err != nil {
return err
}
}
} else {
logger.Log(0, "user with username "+user.Username+" already exists, skipping creation")
continue
}
}
for _, user := range dbUsersMap {
if user.ExternalIdentityProviderID == "" {
continue
}
if _, ok := idpUsersMap[user.UserName]; !ok {
// delete the user if it has been deleted on idp.
err = logic.DeleteUser(user.UserName)
if err != nil {
return err
}
}
}
return nil
}
func syncGroups(idpGroups []idp.Group) error {
dbGroups, err := proLogic.ListUserGroups()
if err != nil && !database.IsEmptyRecord(err) {
return err
}
dbUsers, err := logic.GetUsersDB()
if err != nil && !database.IsEmptyRecord(err) {
return err
}
idpGroupsMap := make(map[string]struct{})
for _, group := range idpGroups {
idpGroupsMap[group.ID] = struct{}{}
}
dbGroupsMap := make(map[string]models.UserGroup)
for _, group := range dbGroups {
if group.ExternalIdentityProviderID != "" {
dbGroupsMap[group.ExternalIdentityProviderID] = group
}
}
dbUsersMap := make(map[string]models.User)
for _, user := range dbUsers {
if user.ExternalIdentityProviderID != "" {
dbUsersMap[user.ExternalIdentityProviderID] = user
}
}
modifiedUsers := make(map[string]struct{})
filters := logic.GetServerSettings().GroupFilters
for _, group := range idpGroups {
var found bool
for _, filter := range filters {
if strings.HasPrefix(group.Name, filter) {
found = true
break
}
}
// if there are filters but none of them match, then skip this group.
if len(filters) > 0 && !found {
continue
}
dbGroup, ok := dbGroupsMap[group.ID]
if !ok {
err := proLogic.CreateUserGroup(models.UserGroup{
ExternalIdentityProviderID: group.ID,
Default: false,
Name: group.Name,
})
if err != nil {
return err
}
} else {
dbGroup.Name = group.Name
err = proLogic.UpdateUserGroup(dbGroup)
if err != nil {
return err
}
}
groupMembersMap := make(map[string]struct{})
for _, member := range group.Members {
groupMembersMap[member] = struct{}{}
}
for _, user := range dbUsers {
// use dbGroup.Name because the group name may have been changed on idp.
_, inNetmakerGroup := user.UserGroups[models.UserGroupID(dbGroup.Name)]
_, inIDPGroup := groupMembersMap[user.ExternalIdentityProviderID]
if inNetmakerGroup && !inIDPGroup {
// use dbGroup.Name because the group name may have been changed on idp.
delete(dbUsersMap[user.ExternalIdentityProviderID].UserGroups, models.UserGroupID(dbGroup.Name))
modifiedUsers[user.ExternalIdentityProviderID] = struct{}{}
}
if !inNetmakerGroup && inIDPGroup {
// use dbGroup.Name because the group name may have been changed on idp.
dbUsersMap[user.ExternalIdentityProviderID].UserGroups[models.UserGroupID(dbGroup.Name)] = struct{}{}
modifiedUsers[user.ExternalIdentityProviderID] = struct{}{}
}
}
}
for userID := range modifiedUsers {
err = logic.UpsertUser(dbUsersMap[userID])
if err != nil {
return err
}
}
for _, group := range dbGroups {
if group.ExternalIdentityProviderID != "" {
if _, ok := idpGroupsMap[group.ExternalIdentityProviderID]; !ok {
// delete the group if it has been deleted on idp.
err = proLogic.DeleteUserGroup(group.ID)
if err != nil {
return err
}
}
}
}
return nil
}

View file

@ -62,6 +62,9 @@ func UserHandlers(r *mux.Router) {
r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))).Methods(http.MethodDelete)
r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGwsV1)))).Methods(http.MethodGet)
r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
r.HandleFunc("/api/idp/sync", logic.SecurityCheck(true, http.HandlerFunc(syncIDP))).Methods(http.MethodPost)
r.HandleFunc("/api/idp", logic.SecurityCheck(true, http.HandlerFunc(removeIDPIntegration))).Methods(http.MethodDelete)
}
// swagger:route POST /api/v1/users/invite-signup user userInviteSignUp
@ -546,6 +549,9 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
userGroup.ExternalIdentityProviderID = currUserG.ExternalIdentityProviderID
err = proLogic.UpdateUserGroup(userGroup)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@ -1423,7 +1429,7 @@ func getPendingUsers(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
users, err := logic.ListPendingUsers()
users, err := logic.ListPendingReturnUsers()
if err != nil {
logger.Log(0, "failed to fetch users: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@ -1461,9 +1467,11 @@ func approvePendingUser(w http.ResponseWriter, r *http.Request) {
return
}
if err = logic.CreateUser(&models.User{
UserName: user.UserName,
Password: newPass,
PlatformRoleID: models.ServiceUser,
UserName: user.UserName,
ExternalIdentityProviderID: user.ExternalIdentityProviderID,
Password: newPass,
AuthType: user.AuthType,
PlatformRoleID: models.ServiceUser,
}); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal"))
return
@ -1505,7 +1513,7 @@ func deletePendingUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
username := params["username"]
users, err := logic.ListPendingUsers()
users, err := logic.ListPendingReturnUsers()
if err != nil {
logger.Log(0, "failed to fetch users: ", err.Error())
@ -1569,3 +1577,82 @@ func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
})
logic.ReturnSuccessResponse(w, r, "cleared all pending users")
}
// @Summary Sync users and groups from idp.
// @Router /api/idp/sync [post]
// @Tags IDP
// @Success 200 {object} models.SuccessResponse
func syncIDP(w http.ResponseWriter, r *http.Request) {
go func() {
err := proAuth.SyncFromIDP()
if err != nil {
logger.Log(0, "failed to sync from idp: ", err.Error())
} else {
logger.Log(0, "sync from idp complete")
}
}()
logic.ReturnSuccessResponse(w, r, "starting sync from idp")
}
// @Summary Remove idp integration.
// @Router /api/idp [delete]
// @Tags IDP
// @Success 200 {object} models.SuccessResponse
// @Failure 500 {object} models.ErrorResponse
func removeIDPIntegration(w http.ResponseWriter, r *http.Request) {
superAdmin, err := logic.GetSuperAdmin()
if err != nil {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("failed to get superadmin: %v", err), "internal"),
)
return
}
if superAdmin.AuthType == models.OAuth {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("cannot remove idp integration with superadmin oauth user"), "badrequest"),
)
return
}
settings := logic.GetServerSettings()
settings.AuthProvider = ""
settings.OIDCIssuer = ""
settings.ClientID = ""
settings.ClientSecret = ""
settings.SyncEnabled = false
settings.GoogleAdminEmail = ""
settings.GoogleSACredsJson = ""
settings.AzureTenant = ""
settings.UserFilters = nil
settings.GroupFilters = nil
err = logic.UpsertServerSettings(settings)
if err != nil {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("failed to remove idp integration: %v", err), "internal"),
)
return
}
proAuth.ResetAuthProvider()
proAuth.ResetIDPSyncHook()
go func() {
err := proAuth.SyncFromIDP()
if err != nil {
logger.Log(0, "failed to sync from idp: ", err.Error())
} else {
logger.Log(0, "sync from idp complete")
}
}()
logic.ReturnSuccessResponse(w, r, "removed idp integration successfully")
}

167
pro/idp/azure/azure.go Normal file
View file

@ -0,0 +1,167 @@
package azure
import (
"encoding/json"
"errors"
"fmt"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/pro/idp"
"net/http"
"net/url"
)
type Client struct {
clientID string
clientSecret string
tenantID string
}
func NewAzureEntraIDClient() *Client {
settings := logic.GetServerSettings()
return &Client{
clientID: settings.ClientID,
clientSecret: settings.ClientSecret,
tenantID: settings.AzureTenant,
}
}
func (a *Client) GetUsers() ([]idp.User, error) {
accessToken, err := a.getAccessToken()
if err != nil {
return nil, err
}
client := &http.Client{}
req, err := http.NewRequest("GET", "https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,displayName,accountEnabled", nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+accessToken)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
var users getUsersResponse
err = json.NewDecoder(resp.Body).Decode(&users)
if err != nil {
return nil, err
}
retval := make([]idp.User, len(users.Value))
for i, user := range users.Value {
retval[i] = idp.User{
ID: user.Id,
Username: user.UserPrincipalName,
DisplayName: user.DisplayName,
AccountDisabled: !user.AccountEnabled,
}
}
return retval, nil
}
func (a *Client) GetGroups() ([]idp.Group, error) {
accessToken, err := a.getAccessToken()
if err != nil {
return nil, err
}
client := &http.Client{}
req, err := http.NewRequest("GET", "https://graph.microsoft.com/v1.0/groups?$select=id,displayName&$expand=members($select=id)", nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+accessToken)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
var groups getGroupsResponse
err = json.NewDecoder(resp.Body).Decode(&groups)
if err != nil {
return nil, err
}
retval := make([]idp.Group, len(groups.Value))
for i, group := range groups.Value {
retvalMembers := make([]string, len(group.Members))
for j, member := range group.Members {
retvalMembers[j] = member.Id
}
retval[i] = idp.Group{
ID: group.Id,
Name: group.DisplayName,
Members: retvalMembers,
}
}
return retval, nil
}
func (a *Client) getAccessToken() (string, error) {
tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", a.tenantID)
var data = url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", a.clientID)
data.Set("client_secret", a.clientSecret)
data.Set("scope", "https://graph.microsoft.com/.default")
resp, err := http.PostForm(tokenURL, data)
if err != nil {
return "", err
}
defer func() {
_ = resp.Body.Close()
}()
var tokenResp map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
if err != nil {
return "", err
}
if token, ok := tokenResp["access_token"].(string); ok {
return token, nil
}
return "", errors.New("failed to get access token")
}
type getUsersResponse struct {
OdataContext string `json:"@odata.context"`
Value []struct {
Id string `json:"id"`
UserPrincipalName string `json:"userPrincipalName"`
DisplayName string `json:"displayName"`
AccountEnabled bool `json:"accountEnabled"`
} `json:"value"`
}
type getGroupsResponse struct {
OdataContext string `json:"@odata.context"`
Value []struct {
Id string `json:"id"`
DisplayName string `json:"displayName"`
Members []struct {
OdataType string `json:"@odata.type"`
Id string `json:"id"`
} `json:"members"`
} `json:"value"`
}

115
pro/idp/google/google.go Normal file
View file

@ -0,0 +1,115 @@
package google
import (
"context"
"encoding/base64"
"encoding/json"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/pro/idp"
admindir "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/impersonate"
"google.golang.org/api/option"
)
type Client struct {
service *admindir.Service
}
func NewGoogleWorkspaceClient() (*Client, error) {
settings := logic.GetServerSettings()
credsJson, err := base64.StdEncoding.DecodeString(settings.GoogleSACredsJson)
if err != nil {
return nil, err
}
credsJsonMap := make(map[string]interface{})
err = json.Unmarshal(credsJson, &credsJsonMap)
if err != nil {
return nil, err
}
source, err := impersonate.CredentialsTokenSource(
context.TODO(),
impersonate.CredentialsConfig{
TargetPrincipal: credsJsonMap["client_email"].(string),
Scopes: []string{
admindir.AdminDirectoryUserReadonlyScope,
admindir.AdminDirectoryGroupReadonlyScope,
admindir.AdminDirectoryGroupMemberReadonlyScope,
},
Subject: settings.GoogleAdminEmail,
},
option.WithCredentialsJSON(credsJson),
)
if err != nil {
return nil, err
}
service, err := admindir.NewService(
context.TODO(),
option.WithTokenSource(source),
)
if err != nil {
return nil, err
}
return &Client{
service: service,
}, nil
}
func (g *Client) GetUsers() ([]idp.User, error) {
var retval []idp.User
err := g.service.Users.List().
Customer("my_customer").
Fields("users(id,primaryEmail,name,suspended)", "nextPageToken").
Pages(context.TODO(), func(users *admindir.Users) error {
for _, user := range users.Users {
retval = append(retval, idp.User{
ID: user.Id,
Username: user.PrimaryEmail,
DisplayName: user.Name.FullName,
AccountDisabled: user.Suspended,
})
}
return nil
})
return retval, err
}
func (g *Client) GetGroups() ([]idp.Group, error) {
var retval []idp.Group
err := g.service.Groups.List().
Customer("my_customer").
Fields("groups(id,name)", "nextPageToken").
Pages(context.TODO(), func(groups *admindir.Groups) error {
for _, group := range groups.Groups {
var retvalMembers []string
err := g.service.Members.List(group.Id).
Fields("members(id)", "nextPageToken").
Pages(context.TODO(), func(members *admindir.Members) error {
for _, member := range members.Members {
retvalMembers = append(retvalMembers, member.Id)
}
return nil
})
if err != nil {
return err
}
retval = append(retval, idp.Group{
ID: group.Id,
Name: group.Name,
Members: retvalMembers,
})
}
return nil
})
return retval, err
}

19
pro/idp/idp.go Normal file
View file

@ -0,0 +1,19 @@
package idp
type Client interface {
GetUsers() ([]User, error)
GetGroups() ([]Group, error)
}
type User struct {
ID string
Username string
DisplayName string
AccountDisabled bool
}
type Group struct {
ID string
Name string
Members []string
}

View file

@ -93,6 +93,7 @@ func InitPro() {
}
proLogic.LoadNodeMetricsToCache()
proLogic.InitFailOverCache()
auth.StartSyncHook()
email.Init()
proLogic.EventWatcher()
})
@ -135,12 +136,14 @@ func InitPro() {
logic.UpdateUserGwAccess = proLogic.UpdateUserGwAccess
logic.CreateDefaultUserPolicies = proLogic.CreateDefaultUserPolicies
logic.MigrateUserRoleAndGroups = proLogic.MigrateUserRoleAndGroups
logic.MigrateGroups = proLogic.MigrateGroups
logic.IntialiseGroups = proLogic.UserGroupsInit
logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins
logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork
logic.GetUserGroup = proLogic.GetUserGroup
logic.GetNodeStatus = proLogic.GetNodeStatus
logic.InitializeAuthProvider = auth.InitializeAuthProvider
logic.ResetAuthProvider = auth.ResetAuthProvider
logic.ResetIDPSyncHook = auth.ResetIDPSyncHook
logic.EmailInit = email.Init
logic.LogEvent = proLogic.LogEvent
}

View file

@ -1,14 +1,75 @@
package logic
import (
"fmt"
"encoding/json"
"github.com/google/uuid"
"github.com/gravitl/netmaker/database"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/models"
)
func MigrateGroups() {
groups, err := ListUserGroups()
if err != nil {
return
}
groupMapping := make(map[models.UserGroupID]models.UserGroupID)
for _, group := range groups {
if group.Default {
continue
}
_, err := uuid.Parse(string(group.ID))
if err == nil {
// group id is already an uuid, so no need to update
continue
}
oldGroupID := group.ID
group.ID = models.UserGroupID(uuid.NewString())
groupMapping[oldGroupID] = group.ID
groupBytes, err := json.Marshal(group)
if err != nil {
continue
}
err = database.Insert(group.ID.String(), string(groupBytes), database.USER_GROUPS_TABLE_NAME)
if err != nil {
continue
}
err = database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, oldGroupID.String())
if err != nil {
continue
}
}
users, err := logic.GetUsersDB()
if err != nil {
return
}
for _, user := range users {
userGroups := make(map[models.UserGroupID]struct{})
for groupID := range user.UserGroups {
newGroupID, ok := groupMapping[groupID]
if !ok {
userGroups[groupID] = struct{}{}
} else {
userGroups[newGroupID] = struct{}{}
}
}
user.UserGroups = userGroups
logic.UpsertUser(user)
}
}
func MigrateUserRoleAndGroups(user models.User) {
var err error
if user.PlatformRoleID == models.AdminRole || user.PlatformRoleID == models.SuperAdminRole {
return
}
@ -20,22 +81,21 @@ func MigrateUserRoleAndGroups(user models.User) {
if err != nil {
continue
}
var g models.UserGroup
var groupID models.UserGroupID
if user.PlatformRoleID == models.ServiceUser {
g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", gwNode.Network, models.NetworkUser)))
groupID = GetDefaultNetworkUserGroupID(models.NetworkID(gwNode.Network))
} else {
g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp",
gwNode.Network, models.NetworkAdmin)))
groupID = GetDefaultNetworkAdminGroupID(models.NetworkID(gwNode.Network))
}
if err != nil {
continue
}
user.UserGroups[g.ID] = struct{}{}
user.UserGroups[groupID] = struct{}{}
}
}
if len(user.NetworkRoles) > 0 {
for netID, netRoles := range user.NetworkRoles {
var g models.UserGroup
var groupID models.UserGroupID
adminAccess := false
for netRoleID := range netRoles {
permTemplate, err := logic.GetRole(netRoleID)
@ -47,19 +107,15 @@ func MigrateUserRoleAndGroups(user models.User) {
}
if user.PlatformRoleID == models.ServiceUser {
g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)))
groupID = GetDefaultNetworkUserGroupID(netID)
} else {
role := models.NetworkUser
if adminAccess {
role = models.NetworkAdmin
groupID = GetDefaultNetworkAdminGroupID(netID)
} else {
groupID = GetDefaultNetworkUserGroupID(netID)
}
g, err = GetUserGroup(models.UserGroupID(fmt.Sprintf("%s-%s-grp",
netID, role)))
}
if err != nil {
continue
}
user.UserGroups[g.ID] = struct{}{}
user.UserGroups[groupID] = struct{}{}
user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"time"
"github.com/gravitl/netmaker/database"
@ -14,6 +15,11 @@ import (
"golang.org/x/exp/slog"
)
var (
globalNetworksAdminGroupID = models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkAdmin))
globalNetworksUserGroupID = models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkUser))
)
var ServiceUserPermissionTemplate = models.UserRolePermissionTemplate{
ID: models.ServiceUser,
Default: true,
@ -111,7 +117,7 @@ func UserRolesInit() {
func UserGroupsInit() {
// create default network groups
var NetworkGlobalAdminGroup = models.UserGroup{
ID: models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkAdmin)),
ID: globalNetworksAdminGroupID,
Default: true,
Name: "All Networks Admin Group",
MetaData: "can manage configuration of all networks",
@ -122,11 +128,11 @@ func UserGroupsInit() {
},
}
var NetworkGlobalUserGroup = models.UserGroup{
ID: models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkUser)),
ID: globalNetworksUserGroupID,
Name: "All Networks User Group",
Default: true,
NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
models.NetworkID(models.AllNetworks): {
models.AllNetworks: {
models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)): {},
},
},
@ -215,7 +221,7 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
// create default network groups
var NetworkAdminGroup = models.UserGroup{
ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)),
ID: GetDefaultNetworkAdminGroupID(netID),
Name: fmt.Sprintf("%s Admin Group", netID),
Default: true,
NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
@ -226,7 +232,7 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
MetaData: fmt.Sprintf("can manage your network `%s` configuration including adding and removing devices.", netID),
}
var NetworkUserGroup = models.UserGroup{
ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)),
ID: GetDefaultNetworkUserGroupID(netID),
Name: fmt.Sprintf("%s User Group", netID),
Default: true,
NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
@ -248,28 +254,29 @@ func DeleteNetworkRoles(netID string) {
if err != nil {
return
}
defaultUserGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)
defaultAdminGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)
defaultAdminGrpID := GetDefaultNetworkAdminGroupID(models.NetworkID(netID))
defaultUserGrpID := GetDefaultNetworkUserGroupID(models.NetworkID(netID))
for _, user := range users {
var upsert bool
if _, ok := user.NetworkRoles[models.NetworkID(netID)]; ok {
delete(user.NetworkRoles, models.NetworkID(netID))
upsert = true
}
if _, ok := user.UserGroups[models.UserGroupID(defaultUserGrp)]; ok {
delete(user.UserGroups, models.UserGroupID(defaultUserGrp))
if _, ok := user.UserGroups[defaultUserGrpID]; ok {
delete(user.UserGroups, defaultUserGrpID)
upsert = true
}
if _, ok := user.UserGroups[models.UserGroupID(defaultAdminGrp)]; ok {
delete(user.UserGroups, models.UserGroupID(defaultAdminGrp))
if _, ok := user.UserGroups[defaultAdminGrpID]; ok {
delete(user.UserGroups, defaultAdminGrpID)
upsert = true
}
if upsert {
logic.UpsertUser(user)
}
}
database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultUserGrp)
database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultAdminGrp)
database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultUserGrpID.String())
database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultAdminGrpID.String())
userGs, _ := ListUserGroups()
for _, userGI := range userGs {
if _, ok := userGI.NetworkRoles[models.NetworkID(netID)]; ok {
@ -524,14 +531,31 @@ func ValidateUpdateGroupReq(g models.UserGroup) error {
// CreateUserGroup - creates new user group
func CreateUserGroup(g models.UserGroup) error {
// check if role already exists
if g.ID == "" {
return errors.New("group id cannot be empty")
// default groups are currently created directly in the db.
// this check is only to prevent future errors.
if g.Default && g.ID == "" {
return errors.New("group id cannot be empty for default group")
}
_, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, g.ID.String())
if err == nil {
return errors.New("group already exists")
if !g.Default {
g.ID = models.UserGroupID(uuid.NewString())
}
// check if the group already exists
if g.Name == "" {
return errors.New("group name cannot be empty")
}
groups, err := ListUserGroups()
if err != nil {
return err
}
for _, group := range groups {
if group.Name == g.Name {
return errors.New("group already exists")
}
}
d, err := json.Marshal(g)
if err != nil {
return err
@ -553,6 +577,14 @@ func GetUserGroup(gid models.UserGroupID) (models.UserGroup, error) {
return ug, nil
}
func GetDefaultNetworkAdminGroupID(networkID models.NetworkID) models.UserGroupID {
return models.UserGroupID(fmt.Sprintf("%s-%s-grp", networkID, models.NetworkAdmin))
}
func GetDefaultNetworkUserGroupID(networkID models.NetworkID) models.UserGroupID {
return models.UserGroupID(fmt.Sprintf("%s-%s-grp", networkID, models.NetworkUser))
}
// ListUserGroups - lists user groups
func ListUserGroups() ([]models.UserGroup, error) {
data, err := database.FetchRecords(database.USER_GROUPS_TABLE_NAME)
@ -573,7 +605,7 @@ func ListUserGroups() ([]models.UserGroup, error) {
// UpdateUserGroup - updates new user group
func UpdateUserGroup(g models.UserGroup) error {
// check if group exists
// check if the group exists
if g.ID == "" {
return errors.New("group id cannot be empty")
}
@ -591,7 +623,7 @@ func UpdateUserGroup(g models.UserGroup) error {
// DeleteUserGroup - deletes user group
func DeleteUserGroup(gid models.UserGroupID) error {
users, err := logic.GetUsersDB()
if err != nil {
if err != nil && !database.IsEmptyRecord(err) {
return err
}
for _, user := range users {
@ -1110,6 +1142,8 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
}
if !logic.IsAclExists(fmt.Sprintf("%s.%s-grp", netID, models.NetworkAdmin)) {
networkAdminGroupID := GetDefaultNetworkAdminGroupID(netID)
defaultUserAcl := models.Acl{
ID: fmt.Sprintf("%s.%s-grp", netID, models.NetworkAdmin),
Name: "Network Admin",
@ -1122,11 +1156,11 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
Src: []models.AclPolicyTag{
{
ID: models.UserGroupAclID,
Value: fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin),
Value: globalNetworksAdminGroupID.String(),
},
{
ID: models.UserGroupAclID,
Value: fmt.Sprintf("global-%s-grp", models.NetworkAdmin),
Value: networkAdminGroupID.String(),
},
},
Dst: []models.AclPolicyTag{
@ -1143,6 +1177,8 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
}
if !logic.IsAclExists(fmt.Sprintf("%s.%s-grp", netID, models.NetworkUser)) {
networkUserGroupID := GetDefaultNetworkUserGroupID(netID)
defaultUserAcl := models.Acl{
ID: fmt.Sprintf("%s.%s-grp", netID, models.NetworkUser),
Name: "Network User",
@ -1155,11 +1191,11 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
Src: []models.AclPolicyTag{
{
ID: models.UserGroupAclID,
Value: fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser),
Value: globalNetworksAdminGroupID.String(),
},
{
ID: models.UserGroupAclID,
Value: fmt.Sprintf("global-%s-grp", models.NetworkUser),
Value: networkUserGroupID.String(),
},
},
@ -1198,5 +1234,6 @@ func AddGlobalNetRolesToAdmins(u *models.User) {
return
}
u.UserGroups = make(map[models.UserGroupID]struct{})
u.UserGroups[models.UserGroupID(fmt.Sprintf("global-%s-grp", models.NetworkAdmin))] = struct{}{}
u.UserGroups[globalNetworksAdminGroupID] = struct{}{}
}