mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-20 12:29:09 +02:00
Super user invites (#483)
This PR brings user invites logic to the Management service via HTTP API. The POST /users/ API endpoint creates a new user in the Idp and then in the local storage. Once the invited user signs ups, the account invitation is redeemed. There are a few limitations. This works only with an enabled IdP manager. Users that already have a registered account can't be invited.
This commit is contained in:
@@ -35,6 +35,10 @@ components:
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
status:
|
||||
description: User's status
|
||||
type: string
|
||||
enum: [ "active","invited","disabled" ]
|
||||
auto_groups:
|
||||
description: Groups to auto-assign to peers registered by this user
|
||||
type: array
|
||||
@@ -46,6 +50,7 @@ components:
|
||||
- name
|
||||
- role
|
||||
- auto_groups
|
||||
- status
|
||||
UserRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -60,6 +65,27 @@ components:
|
||||
required:
|
||||
- role
|
||||
- auto_groups
|
||||
UserCreateRequest:
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
description: User's NetBird account role
|
||||
type: string
|
||||
email:
|
||||
description: User's Email to send invite to
|
||||
type: string
|
||||
name:
|
||||
description: User's full name
|
||||
type: string
|
||||
auto_groups:
|
||||
description: Groups to auto-assign to peers registered by this user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- role
|
||||
- auto_groups
|
||||
- email
|
||||
PeerMinimum:
|
||||
type: object
|
||||
properties:
|
||||
@@ -499,6 +525,33 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/users/:
|
||||
post:
|
||||
summary: Create a User (invite)
|
||||
tags: [ Users]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
requestBody:
|
||||
description: User invite information
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserCreateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A User object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/users/{id}:
|
||||
put:
|
||||
summary: Update information about a User
|
||||
|
||||
@@ -87,6 +87,13 @@ const (
|
||||
RulePatchOperationPathSources RulePatchOperationPath = "sources"
|
||||
)
|
||||
|
||||
// Defines values for UserStatus.
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusDisabled UserStatus = "disabled"
|
||||
UserStatusInvited UserStatus = "invited"
|
||||
)
|
||||
|
||||
// Group defines model for Group.
|
||||
type Group struct {
|
||||
// Id Group ID
|
||||
@@ -466,6 +473,27 @@ type User struct {
|
||||
|
||||
// Role User's NetBird account role
|
||||
Role string `json:"role"`
|
||||
|
||||
// Status User's status
|
||||
Status UserStatus `json:"status"`
|
||||
}
|
||||
|
||||
// UserStatus User's status
|
||||
type UserStatus string
|
||||
|
||||
// UserCreateRequest defines model for UserCreateRequest.
|
||||
type UserCreateRequest struct {
|
||||
// AutoGroups Groups to auto-assign to peers registered by this user
|
||||
AutoGroups []string `json:"auto_groups"`
|
||||
|
||||
// Email User's Email to send invite to
|
||||
Email string `json:"email"`
|
||||
|
||||
// Name User's full name
|
||||
Name *string `json:"name,omitempty"`
|
||||
|
||||
// Role User's NetBird account role
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// UserRequest defines model for UserRequest.
|
||||
@@ -586,5 +614,8 @@ type PostApiSetupKeysJSONRequestBody = SetupKeyRequest
|
||||
// PutApiSetupKeysIdJSONRequestBody defines body for PutApiSetupKeysId for application/json ContentType.
|
||||
type PutApiSetupKeysIdJSONRequestBody = SetupKeyRequest
|
||||
|
||||
// PostApiUsersJSONRequestBody defines body for PostApiUsers for application/json ContentType.
|
||||
type PostApiUsersJSONRequestBody = UserCreateRequest
|
||||
|
||||
// PutApiUsersIdJSONRequestBody defines body for PutApiUsersId for application/json ContentType.
|
||||
type PutApiUsersIdJSONRequestBody = UserRequest
|
||||
|
||||
@@ -67,14 +67,14 @@ func initGroupTestData(groups ...*server.Group) *Groups {
|
||||
}
|
||||
return nil, fmt.Errorf("peer not found")
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return &server.Account{
|
||||
Id: claims.AccountId,
|
||||
Domain: "hotmail.com",
|
||||
Peers: TestPeers,
|
||||
Groups: map[string]*server.Group{
|
||||
"id-existed": &server.Group{ID: "id-existed", Peers: []string{"A", "B"}},
|
||||
"id-all": &server.Group{ID: "id-all", Name: "All"}},
|
||||
"id-existed": {ID: "id-existed", Peers: []string{"A", "B"}},
|
||||
"id-all": {ID: "id-all", Name: "All"}},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
|
||||
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/users/{id}", userHandler.UpdateUser).Methods("PUT", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/users", userHandler.CreateUserHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetAllSetupKeysHandler).Methods("GET", "OPTIONS")
|
||||
apiHandler.HandleFunc("/api/setup-keys", keysHandler.CreateSetupKeyHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
@@ -104,7 +104,7 @@ func initNameserversTestData() *Nameservers {
|
||||
}
|
||||
return nsGroupToUpdate, nil
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return testingNSAccount, nil
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
func initTestMetaData(peer ...*server.Peer) *Peers {
|
||||
return &Peers{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return &server.Account{
|
||||
Id: claims.AccountId,
|
||||
Domain: "hotmail.com",
|
||||
|
||||
@@ -120,7 +120,7 @@ func initRoutesTestData() *Routes {
|
||||
}
|
||||
return routeToUpdate, nil
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return testingAccount, nil
|
||||
},
|
||||
},
|
||||
|
||||
@@ -66,14 +66,14 @@ func initRulesTestData(rules ...*server.Rule) *Rules {
|
||||
}
|
||||
return &rule, nil
|
||||
},
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return &server.Account{
|
||||
Id: claims.AccountId,
|
||||
Domain: "hotmail.com",
|
||||
Rules: map[string]*server.Rule{"id-existed": &server.Rule{ID: "id-existed"}},
|
||||
Groups: map[string]*server.Group{
|
||||
"F": &server.Group{ID: "F"},
|
||||
"G": &server.Group{ID: "G"},
|
||||
"F": {ID: "F"},
|
||||
"G": {ID: "G"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.SetupKey, updatedSetupKey *server.SetupKey) *SetupKeys {
|
||||
return &SetupKeys{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
return &server.Account{
|
||||
Id: testAccountID,
|
||||
Domain: "hotmail.com",
|
||||
|
||||
@@ -5,12 +5,11 @@ import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/netbirdio/netbird/management/server/http/api"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"net/http"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
)
|
||||
@@ -82,6 +81,50 @@ func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
// CreateUserHandler creates a User in the system with a status "invited" (effectively this is a user invite).
|
||||
func (h *UserHandler) CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
req := &api.PostApiUsersJSONRequestBody{}
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if server.StrRoleToUserRole(req.Role) == server.UserRoleUnknown {
|
||||
http.Error(w, "unknown user role "+req.Role, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newUser, err := h.accountManager.CreateUser(account.Id, &server.UserInfo{
|
||||
Email: req.Email,
|
||||
Name: *req.Name,
|
||||
Role: req.Role,
|
||||
AutoGroups: req.AutoGroups,
|
||||
})
|
||||
if err != nil {
|
||||
if e, ok := server.FromError(err); ok {
|
||||
switch e.Type() {
|
||||
case server.UserAlreadyExists:
|
||||
http.Error(w, "You can't invite users with an existing NetBird account.", http.StatusPreconditionFailed)
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
http.Error(w, "failed to invite", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSONObject(w, toUserResponse(newUser))
|
||||
}
|
||||
|
||||
// GetUsers returns a list of users of the account this user belongs to.
|
||||
// It also gathers additional user data (like email and name) from the IDP manager.
|
||||
func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -101,7 +144,7 @@ func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
users := []*api.User{}
|
||||
users := make([]*api.User, 0)
|
||||
for _, r := range data {
|
||||
users = append(users, toUserResponse(r))
|
||||
}
|
||||
@@ -116,11 +159,22 @@ func toUserResponse(user *server.UserInfo) *api.User {
|
||||
autoGroups = []string{}
|
||||
}
|
||||
|
||||
var userStatus api.UserStatus
|
||||
switch user.Status {
|
||||
case "active":
|
||||
userStatus = api.UserStatusActive
|
||||
case "invited":
|
||||
userStatus = api.UserStatusInvited
|
||||
default:
|
||||
userStatus = api.UserStatusDisabled
|
||||
}
|
||||
|
||||
return &api.User{
|
||||
Id: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
AutoGroups: autoGroups,
|
||||
Status: userStatus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
func initUsers(user ...*server.User) *UserHandler {
|
||||
return &UserHandler{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||
users := make(map[string]*server.User, 0)
|
||||
for _, u := range user {
|
||||
users[u.Id] = u
|
||||
|
||||
@@ -60,7 +60,7 @@ func getJWTAccount(accountManager server.AccountManager,
|
||||
|
||||
jwtClaims := jwtExtractor.ExtractClaimsFromRequestContext(r, authAudience)
|
||||
|
||||
account, err := accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
|
||||
account, err := accountManager.GetAccountFromToken(jwtClaims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user