Skip to content

Commit

Permalink
Merging full service user feature into main (netbirdio#819)
Browse files Browse the repository at this point in the history
Merging full feature branch into main.
Adding full support for service users including backend objects, persistence, verification and api endpoints.
  • Loading branch information
pascal-fischer authored Apr 22, 2023
1 parent 0699192 commit 7589d0d
Show file tree
Hide file tree
Showing 11 changed files with 934 additions and 159 deletions.
27 changes: 16 additions & 11 deletions management/server/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,17 @@ type AccountManager interface {
CreateSetupKey(accountID string, keyName string, keyType SetupKeyType, expiresIn time.Duration,
autoGroups []string, usageLimit int, userID string) (*SetupKey, error)
SaveSetupKey(accountID string, key *SetupKey, userID string) (*SetupKey, error)
CreateUser(accountID, userID string, key *UserInfo) (*UserInfo, error)
CreateUser(accountID, executingUserID string, key *UserInfo) (*UserInfo, error)
DeleteUser(accountID, executingUserID string, targetUserID string) error
ListSetupKeys(accountID, userID string) ([]*SetupKey, error)
SaveUser(accountID, userID string, update *User) (*UserInfo, error)
GetSetupKey(accountID, userID, keyID string) (*SetupKey, error)
GetAccountByUserOrAccountID(userID, accountID, domain string) (*Account, error)
GetAccountByUserID(userID string) (*Account, error)
GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, *User, error)
GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error)
MarkPATUsed(tokenID string) error
IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error)
IsUserAdmin(userID string) (bool, error)
AccountExists(accountId string) (*bool, error)
GetPeerByKey(peerKey string) (*Peer, error)
GetPeers(accountID, userID string) ([]*Peer, error)
Expand Down Expand Up @@ -171,12 +173,13 @@ type Account struct {
}

type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Role string `json:"role"`
AutoGroups []string `json:"auto_groups"`
Status string `json:"-"`
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Role string `json:"role"`
AutoGroups []string `json:"auto_groups"`
Status string `json:"-"`
IsServiceUser bool `json:"is_service_user"`
}

// getRoutesToSync returns the enabled routes for the peer ID and the routes
Expand Down Expand Up @@ -1228,9 +1231,11 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat
return nil, nil, status.Errorf(status.NotFound, "user %s not found", claims.UserId)
}

err = am.redeemInvite(account, claims.UserId)
if err != nil {
return nil, nil, err
if !user.IsServiceUser {
err = am.redeemInvite(account, claims.UserId)
if err != nil {
return nil, nil, err
}
}

return account, user, nil
Expand Down
16 changes: 16 additions & 0 deletions management/server/activity/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ const (
PersonalAccessTokenCreated
// PersonalAccessTokenDeleted indicates that a user deleted a personal access token
PersonalAccessTokenDeleted
// ServiceUserCreated indicates that a user created a service user
ServiceUserCreated
// ServiceUserDeleted indicates that a user deleted a service user
ServiceUserDeleted
)

const (
Expand Down Expand Up @@ -176,6 +180,10 @@ const (
PersonalAccessTokenCreatedMessage string = "Personal access token created"
// PersonalAccessTokenDeletedMessage is a human-readable text message of the PersonalAccessTokenDeleted activity
PersonalAccessTokenDeletedMessage string = "Personal access token deleted"
// ServiceUserCreatedMessage is a human-readable text message of the ServiceUserCreated activity
ServiceUserCreatedMessage string = "Service user created"
// ServiceUserDeletedMessage is a human-readable text message of the ServiceUserDeleted activity
ServiceUserDeletedMessage string = "Service user deleted"
)

// Activity that triggered an Event
Expand Down Expand Up @@ -270,6 +278,10 @@ func (a Activity) Message() string {
return PersonalAccessTokenCreatedMessage
case PersonalAccessTokenDeleted:
return PersonalAccessTokenDeletedMessage
case ServiceUserCreated:
return ServiceUserCreatedMessage
case ServiceUserDeleted:
return ServiceUserDeletedMessage
default:
return "UNKNOWN_ACTIVITY"
}
Expand Down Expand Up @@ -364,6 +376,10 @@ func (a Activity) StringCode() string {
return "personal.access.token.create"
case PersonalAccessTokenDeleted:
return "personal.access.token.delete"
case ServiceUserCreated:
return "service.user.create"
case ServiceUserDeleted:
return "service.user.delete"
default:
return "UNKNOWN_ACTIVITY"
}
Expand Down
39 changes: 38 additions & 1 deletion management/server/http/api/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ components:
description: Is true if authenticated user is the same as this user
type: boolean
readOnly: true
is_service_user:
description: Is true if this user is a service user
type: boolean
readOnly: true
required:
- id
- email
Expand Down Expand Up @@ -115,10 +119,13 @@ components:
type: array
items:
type: string
is_service_user:
description: Is true if this user is a service user
type: boolean
required:
- role
- auto_groups
- email
- is_service_user
PeerMinimum:
type: object
properties:
Expand Down Expand Up @@ -825,6 +832,12 @@ paths:
tags: [ Users ]
security:
- BearerAuth: [ ]
parameters:
- in: query
name: service_user
schema:
type: boolean
description: Filters users and returns either normal users or service users
responses:
'200':
description: A JSON array of Users
Expand Down Expand Up @@ -903,6 +916,30 @@ paths:
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
delete:
summary: Delete a User
tags: [ Users ]
security:
- BearerAuth: [ ]
parameters:
- in: path
name: id
required: true
schema:
type: string
description: The User ID
responses:
'200':
description: Delete status code
content: { }
'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/{userId}/tokens:
get:
summary: Returns a list of all tokens for a user
Expand Down
14 changes: 13 additions & 1 deletion management/server/http/api/types.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions management/server/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func (apiHandler *apiHandler) addUsersEndpoint() {
userHandler := NewUsersHandler(apiHandler.AccountManager, apiHandler.AuthCfg)
apiHandler.Router.HandleFunc("/users", userHandler.GetAllUsers).Methods("GET", "OPTIONS")
apiHandler.Router.HandleFunc("/users/{id}", userHandler.UpdateUser).Methods("PUT", "OPTIONS")
apiHandler.Router.HandleFunc("/users/{id}", userHandler.DeleteUser).Methods("DELETE", "OPTIONS")
apiHandler.Router.HandleFunc("/users", userHandler.CreateUser).Methods("POST", "OPTIONS")
}

Expand Down
4 changes: 2 additions & 2 deletions management/server/http/middleware/access_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/netbirdio/netbird/management/server/jwtclaims"
)

type IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error)
type IsUserAdminFunc func(userID string) (bool, error)

// AccessControl middleware to restrict to make POST/PUT/DELETE requests by admin only
type AccessControl struct {
Expand All @@ -37,7 +37,7 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := a.claimsExtract.FromRequestContext(r)

ok, err := a.isUserAdmin(claims)
ok, err := a.isUserAdmin(claims.UserId)
if err != nil {
util.WriteError(status.Errorf(status.Unauthorized, "invalid JWT"), w)
return
Expand Down
81 changes: 69 additions & 12 deletions management/server/http/users_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package http
import (
"encoding/json"
"net/http"
"strconv"

"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"

"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/http/util"
Expand Down Expand Up @@ -77,6 +79,36 @@ func (h *UsersHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
util.WriteJSONObject(w, toUserResponse(newUser, claims.UserId))
}

// DeleteUser is a DELETE request to delete a user (only works for service users right now)
func (h *UsersHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
return
}

claims := h.claimsExtractor.FromRequestContext(r)
account, user, err := h.accountManager.GetAccountFromToken(claims)
if err != nil {
util.WriteError(err, w)
return
}

vars := mux.Vars(r)
targetUserID := vars["id"]
if len(targetUserID) == 0 {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid user ID"), w)
return
}

err = h.accountManager.DeleteUser(account.Id, user.Id, targetUserID)
if err != nil {
util.WriteError(err, w)
return
}

util.WriteJSONObject(w, emptyObject{})
}

// CreateUser creates a User in the system with a status "invited" (effectively this is a user invite).
func (h *UsersHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
Expand All @@ -103,11 +135,17 @@ func (h *UsersHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
return
}

email := ""
if req.Email != nil {
email = *req.Email
}

newUser, err := h.accountManager.CreateUser(account.Id, user.Id, &server.UserInfo{
Email: req.Email,
Name: *req.Name,
Role: req.Role,
AutoGroups: req.AutoGroups,
Email: email,
Name: *req.Name,
Role: req.Role,
AutoGroups: req.AutoGroups,
IsServiceUser: req.IsServiceUser,
})
if err != nil {
util.WriteError(err, w)
Expand Down Expand Up @@ -137,9 +175,27 @@ func (h *UsersHandler) GetAllUsers(w http.ResponseWriter, r *http.Request) {
return
}

serviceUser := r.URL.Query().Get("service_user")

log.Debugf("UserCount: %v", len(data))

users := make([]*api.User, 0)
for _, r := range data {
users = append(users, toUserResponse(r, claims.UserId))
if serviceUser == "" {
users = append(users, toUserResponse(r, claims.UserId))
continue
}
includeServiceUser, err := strconv.ParseBool(serviceUser)
log.Debugf("Should include service user: %v", includeServiceUser)
if err != nil {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid service_user query parameter"), w)
return
}
log.Debugf("User %v is service user: %v", r.Name, r.IsServiceUser)
if includeServiceUser == r.IsServiceUser {
log.Debugf("Found service user: %v", r.Name)
users = append(users, toUserResponse(r, claims.UserId))
}
}

util.WriteJSONObject(w, users)
Expand All @@ -163,12 +219,13 @@ func toUserResponse(user *server.UserInfo, currenUserID string) *api.User {

isCurrent := user.ID == currenUserID
return &api.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
Role: user.Role,
AutoGroups: autoGroups,
Status: userStatus,
IsCurrent: &isCurrent,
Id: user.ID,
Name: user.Name,
Email: user.Email,
Role: user.Role,
AutoGroups: autoGroups,
Status: userStatus,
IsCurrent: &isCurrent,
IsServiceUser: &user.IsServiceUser,
}
}
Loading

0 comments on commit 7589d0d

Please sign in to comment.