Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merging full service user feature into main #819

Merged
merged 17 commits into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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