add rest endpoints and update openapi doc

This commit is contained in:
Pascal Fischer 2023-03-21 16:02:19 +01:00
parent 9c5adfea2b
commit de8608f99f
5 changed files with 442 additions and 0 deletions

View File

@ -6,6 +6,8 @@ info:
tags:
- name: Users
description: Interact with and view information about users.
- name: Tokens
description: Interact with and view information about tokens.
- name: Peers
description: Interact with and view information about peers.
- name: Setup Keys
@ -284,6 +286,53 @@ components:
- revoked
- auto_groups
- usage_limit
PersonalAccessToken:
type: object
properties:
id:
description: ID of a token
type: string
description:
description: Description of the token
type: string
# hashed_token:
# description: Hashed representation of the token
# type: string
expiration_date:
description: Date the token expires
type: string
format: date-time
created_by:
description: User ID of the user who created the token
type: string
created_at:
description: Date the token was created
type: string
format: date-time
last_used:
description: Date the token was last used
type: string
format: date-time
required:
- id
- description
# - hashed_token
- expiration_date
- created_by
- created_at
- last_used
PersonalAccessTokenRequest:
type: object
properties:
description:
description: Description of the token
type: string
expires_in:
description: Expiration in days
type: integer
required:
- description
- expires_in
GroupMinimum:
type: object
properties:
@ -848,6 +897,133 @@ paths:
"$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
tags: [ Tokens ]
security:
- BearerAuth: []
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The User ID
responses:
'200':
description: A JSON Array of PersonalAccessTokens
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PersonalAccessToken'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
post:
summary: Create a new token
tags: [ Tokens ]
security:
- BearerAuth: [ ]
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The User ID
requestBody:
description: PersonalAccessToken create parameters
content:
application/json:
schema:
$ref: '#/components/schemas/PersonalAccessTokenRequest'
responses:
'200':
description: The token in plain text
content:
text/plain:
schema:
type: string
'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/{tokenId}:
get:
summary: Returns a specific token
tags: [ Tokens ]
security:
- BearerAuth: [ ]
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The User ID
- in: path
name: tokenId
required: true
schema:
type: string
description: The Token ID
responses:
'200':
description: A PersonalAccessTokens Object
content:
application/json:
schema:
$ref: '#/components/schemas/PersonalAccessToken'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
delete:
summary: Delete a token
tags: [ Tokens ]
security:
- BearerAuth: [ ]
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The User ID
- in: path
name: tokenId
required: true
schema:
type: string
description: The Token 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/peers:
get:
summary: Returns a list of all peers

View File

@ -379,6 +379,36 @@ type PeerMinimum struct {
Name string `json:"name"`
}
// PersonalAccessToken defines model for PersonalAccessToken.
type PersonalAccessToken struct {
// CreatedAt Date the token was created
CreatedAt time.Time `json:"created_at"`
// CreatedBy User ID of the user who created the token
CreatedBy string `json:"created_by"`
// Description Description of the token
Description string `json:"description"`
// ExpirationDate Date the token expires
ExpirationDate time.Time `json:"expiration_date"`
// Id ID of a token
Id string `json:"id"`
// LastUsed Date the token was last used
LastUsed time.Time `json:"last_used"`
}
// PersonalAccessTokenRequest defines model for PersonalAccessTokenRequest.
type PersonalAccessTokenRequest struct {
// Description Description of the token
Description string `json:"description"`
// ExpiresIn Expiration in days
ExpiresIn int `json:"expires_in"`
}
// Policy defines model for Policy.
type Policy struct {
// Description Policy friendly description
@ -808,3 +838,6 @@ type PostApiUsersJSONRequestBody = UserCreateRequest
// PutApiUsersIdJSONRequestBody defines body for PutApiUsersId for application/json ContentType.
type PutApiUsersIdJSONRequestBody = UserRequest
// PostApiUsersUserIdTokensJSONRequestBody defines body for PostApiUsersUserIdTokens for application/json ContentType.
type PostApiUsersUserIdTokensJSONRequestBody = PersonalAccessTokenRequest

View File

@ -57,6 +57,7 @@ func APIHandler(accountManager s.AccountManager, appMetrics telemetry.AppMetrics
api.addAccountsEndpoint()
api.addPeersEndpoint()
api.addUsersEndpoint()
api.addUsersTokensEndpoint()
api.addSetupKeysEndpoint()
api.addRulesEndpoint()
api.addPoliciesEndpoint()
@ -110,6 +111,14 @@ func (apiHandler *apiHandler) addUsersEndpoint() {
apiHandler.Router.HandleFunc("/users", userHandler.CreateUser).Methods("POST", "OPTIONS")
}
func (apiHandler *apiHandler) addUsersTokensEndpoint() {
tokenHandler := NewPATsHandler(apiHandler.AccountManager, apiHandler.AuthCfg)
apiHandler.Router.HandleFunc("/users/{userId}/tokens", tokenHandler.GetAllTokens).Methods("GET", "OPTIONS")
apiHandler.Router.HandleFunc("/users/{userId}/tokens", tokenHandler.CreateToken).Methods("POST", "OPTIONS")
apiHandler.Router.HandleFunc("/users/{userId}/tokens/{tokenId}", tokenHandler.GetToken).Methods("GET", "OPTIONS")
apiHandler.Router.HandleFunc("/users/{userId}/tokens/{tokenId}", tokenHandler.DeleteToken).Methods("DELETE", "OPTIONS")
}
func (apiHandler *apiHandler) addSetupKeysEndpoint() {
keysHandler := NewSetupKeysHandler(apiHandler.AccountManager, apiHandler.AuthCfg)
apiHandler.Router.HandleFunc("/setup-keys", keysHandler.GetAllSetupKeys).Methods("GET", "OPTIONS")

View File

@ -0,0 +1,187 @@
package http
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
)
// PATHandler is the nameserver group handler of the account
type PATHandler struct {
accountManager server.AccountManager
claimsExtractor *jwtclaims.ClaimsExtractor
}
func NewPATsHandler(accountManager server.AccountManager, authCfg AuthCfg) *PATHandler {
return &PATHandler{
accountManager: accountManager,
claimsExtractor: jwtclaims.NewClaimsExtractor(
jwtclaims.WithAudience(authCfg.Audience),
jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
),
}
}
func (h *PATHandler) GetAllTokens(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
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)
userID := vars["userId"]
if len(userID) == 0 {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid user ID"), w)
return
}
if userID != user.Id {
util.WriteErrorResponse("User not authorized to get tokens", http.StatusUnauthorized, w)
return
}
var pats []*api.PersonalAccessToken
for _, pat := range account.Users[userID].PATs {
pats = append(pats, toPATResponse(pat))
}
util.WriteJSONObject(w, pats)
}
func (h *PATHandler) GetToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
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)
userID := vars["userId"]
if len(userID) == 0 {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid user ID"), w)
return
}
if userID != user.Id {
util.WriteErrorResponse("User not authorized to get token", http.StatusUnauthorized, w)
return
}
tokenID := vars["tokenId"]
if len(tokenID) == 0 {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid token ID"), w)
return
}
pat := account.Users[userID].PATs[tokenID]
util.WriteJSONObject(w, toPATResponse(pat))
}
func (h *PATHandler) CreateToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
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)
userID := vars["userId"]
if len(userID) == 0 {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid user ID"), w)
return
}
if userID != user.Id {
util.WriteErrorResponse("User not authorized to create token", http.StatusUnauthorized, w)
return
}
var req api.PostApiUsersUserIdTokensJSONRequestBody
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
return
}
pat, plainToken, err := server.CreateNewPAT(req.Description, req.ExpiresIn, user.Id)
err = h.accountManager.AddPATToUser(account.Id, userID, pat)
if err != nil {
util.WriteError(err, w)
return
}
util.WriteJSONObject(w, plainToken)
}
func (h *PATHandler) DeleteToken(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)
userID := vars["userId"]
if len(userID) == 0 {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid user ID"), w)
return
}
if userID != user.Id {
util.WriteErrorResponse("User not authorized to delete token", http.StatusUnauthorized, w)
return
}
tokenID := vars["tokenId"]
if len(tokenID) == 0 {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid token ID"), w)
return
}
err = h.accountManager.DeletePAT(account.Id, userID, tokenID)
if err != nil {
util.WriteError(err, w)
return
}
util.WriteJSONObject(w, "")
}
func toPATResponse(pat *server.PersonalAccessToken) *api.PersonalAccessToken {
return &api.PersonalAccessToken{
CreatedAt: pat.CreatedAt,
CreatedBy: pat.CreatedBy,
Description: pat.Description,
ExpirationDate: pat.ExpirationDate,
Id: pat.ID,
LastUsed: pat.LastUsed,
}
}

View File

@ -0,0 +1,37 @@
package http
import (
"net/http"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/status"
)
func initPATTestData() *PATHandler {
return &PATHandler{
accountManager: &mock_server.MockAccountManager{
AddPATToUserFunc: func(accountID string, userID string, pat *server.PersonalAccessToken) error {
if nsGroupID == existingNSGroupID {
return baseExistingNSGroup.Copy(), nil
}
return nil, status.Errorf(status.NotFound, "nameserver group with ID %s not found", nsGroupID)
},
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
return testingNSAccount, testingAccount.Users["test_user"], nil
},
},
claimsExtractor: jwtclaims.NewClaimsExtractor(
jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
return jwtclaims.AuthorizationClaims{
UserId: "test_user",
Domain: "hotmail.com",
AccountId: testNSGroupAccountID,
}
}),
),
}
}