Ver Fonte

feat: api access tokens

abhishek9686 há 5 meses atrás
pai
commit
6c15dc49d8
8 ficheiros alterados com 198 adições e 6 exclusões
  1. 112 0
      controllers/user.go
  2. 3 0
      database/database.go
  3. 44 0
      logic/auth.go
  4. 23 0
      logic/jwts.go
  5. 13 6
      models/accessToken.go
  6. 1 0
      models/structs.go
  7. 1 0
      models/user_mgmt.go
  8. 1 0
      servercfg/serverconf.go

+ 112 - 0
controllers/user.go

@@ -37,7 +37,119 @@ func userHandlers(r *mux.Router) {
 	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)
+	r.HandleFunc("/api/v1/users/{username}/access_token", logic.SecurityCheck(true, http.HandlerFunc(createUserAccessToken))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/{username}/access_token", logic.SecurityCheck(true, http.HandlerFunc(getUserAccessTokens))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/{username}/access_token", logic.SecurityCheck(true, http.HandlerFunc(deleteUserAccessTokens))).Methods(http.MethodDelete)
+}
+
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/v1/users/{username}/access_token [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
+
+	// Auth request consists of Mac Address and Password (from node that is authorizing
+	// in case of Master, auth is ignored and mac is set to "mastermac"
+	var req models.AccessToken
+
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	user, err := logic.GetUser(req.UserName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	if logic.IsOauthUser(user) == nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
+		return
+	}
+	jwt, err := logic.CreateUserJWTWithExpiry(user.UserName, user.PlatformRoleID, req.ExpiresAt)
+
+	if jwt == "" {
+		// very unlikely that err is !nil and no jwt returned, but handle it anyways.
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating access token "+err.Error()), "internal"),
+		)
+		return
+	}
+	err = logic.CreateAccessToken(req)
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating access token "+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
+		AuthToken: jwt,
+		UserName:  req.UserName,
+	}, "api access token has generated for user "+req.UserName)
+}
 
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/v1/users/{username}/access_token [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func getUserAccessTokens(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	username := params["username"]
+	_, err := logic.GetUser(username)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		return
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, logic.ListAccessTokens(username), "fetched api access tokens for user "+username)
+}
+
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/v1/users/{username}/access_token [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
+	var req models.AccessToken
+
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.RevokeAccessToken(req)
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating access token "+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token "+req.Name)
 }
 
 // @Summary     Authenticate a user to retrieve an authorization token

+ 3 - 0
database/database.go

@@ -25,6 +25,8 @@ 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 = "access_tokens"
 	// USER_PERMISSIONS_TABLE_NAME - user permissions table
 	USER_PERMISSIONS_TABLE_NAME = "user_permissions"
 	// CERTS_TABLE_NAME - certificates table
@@ -140,6 +142,7 @@ func createTables() {
 	CreateTable(CERTS_TABLE_NAME)
 	CreateTable(DELETED_NODES_TABLE_NAME)
 	CreateTable(USERS_TABLE_NAME)
+	CreateTable(ACCESS_TOKENS_TABLE_NAME)
 	CreateTable(DNS_TABLE_NAME)
 	CreateTable(EXT_CLIENT_TABLE_NAME)
 	CreateTable(PEERS_TABLE_NAME)

+ 44 - 0
logic/auth.go

@@ -138,6 +138,50 @@ func FetchPassValue(newValue string) (string, error) {
 	return string(b64CurrentValue), nil
 }
 
+func RevokeAccessToken(a models.AccessToken) error {
+	err := database.DeleteRecord(database.ACCESS_TOKENS_TABLE_NAME, a.GetDBKey())
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func ListAccessTokens(username string) (tokens []models.AccessToken) {
+	collection, err := database.FetchRecords(database.USERS_TABLE_NAME)
+	if err != nil {
+		return
+	}
+
+	for _, value := range collection {
+
+		var a models.AccessToken
+		err = json.Unmarshal([]byte(value), &a)
+		if err != nil {
+			continue // get users
+		}
+		if a.UserName == username {
+			tokens = append(tokens, a)
+		}
+
+	}
+	return
+}
+
+func CreateAccessToken(a models.AccessToken) error {
+	// connect db
+	data, err := json.Marshal(a)
+	if err != nil {
+		logger.Log(0, "failed to marshal", err.Error())
+		return err
+	}
+	err = database.Insert(a.GetDBKey(), string(data), database.ACCESS_TOKENS_TABLE_NAME)
+	if err != nil {
+		logger.Log(0, "failed to insert user", err.Error())
+		return err
+	}
+	return nil
+}
+
 // CreateUser - creates a user
 func CreateUser(user *models.User) error {
 	// check if user exists

+ 23 - 0
logic/jwts.go

@@ -52,6 +52,29 @@ func CreateJWT(uuid string, macAddress string, network string) (response string,
 	return "", err
 }
 
+// CreateUserJWT - creates a user jwt token
+func CreateUserJWTWithExpiry(username string, role models.UserRoleID, d time.Time) (response string, err error) {
+	claims := &models.UserClaims{
+		UserName:       username,
+		Role:           role,
+		Api:            servercfg.ServerInfo.APIHost,
+		RacAutoDisable: servercfg.GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole),
+		RegisteredClaims: jwt.RegisteredClaims{
+			Issuer:    "Netmaker",
+			Subject:   fmt.Sprintf("user|%s", username),
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			ExpiresAt: jwt.NewNumericDate(d),
+		},
+	}
+
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	tokenString, err := token.SignedString(jwtSecretKey)
+	if err == nil {
+		return tokenString, nil
+	}
+	return "", err
+}
+
 // CreateUserJWT - creates a user jwt token
 func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
 	expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration)

+ 13 - 6
models/accessToken.go

@@ -1,13 +1,20 @@
 package models
 
+import (
+	"fmt"
+	"time"
+)
+
 // AccessToken - token used to access netmaker
 type AccessToken struct {
-	APIConnString string `json:"apiconnstring"`
-	ClientConfig
+	Name      string    `json:"name"`
+	UserName  string    `json:"user_name"`
+	ExpiresAt time.Time `json:"expires_at"`
+	LastUsed  time.Time `json:"last_used"`
+	CreatedBy time.Time `json:"created_by"`
+	CreatedAt time.Time `json:"created_at"`
 }
 
-// ClientConfig - the config of the client
-type ClientConfig struct {
-	Network string `json:"network"`
-	Key     string `json:"key"`
+func (a AccessToken) GetDBKey() string {
+	return fmt.Sprintf("%s#%s", a.UserName, a.Name)
 }

+ 1 - 0
models/structs.go

@@ -263,6 +263,7 @@ type NodeJoinResponse struct {
 type ServerConfig struct {
 	CoreDNSAddr       string `yaml:"corednsaddr"`
 	API               string `yaml:"api"`
+	APIHost           string `yaml:"apihost"`
 	APIPort           string `yaml:"apiport"`
 	DNSMode           string `yaml:"dnsmode"`
 	Version           string `yaml:"version"`

+ 1 - 0
models/user_mgmt.go

@@ -185,6 +185,7 @@ type UserAuthParams struct {
 type UserClaims struct {
 	Role           UserRoleID
 	UserName       string
+	Api            string
 	RacAutoDisable bool
 	jwt.RegisteredClaims
 }

+ 1 - 0
servercfg/serverconf.go

@@ -137,6 +137,7 @@ func GetServerInfo() models.ServerConfig {
 		cfg.MQUserName = GetMqUserName()
 		cfg.MQPassword = GetMqPassword()
 	}
+	cfg.APIHost = GetAPIHost()
 	cfg.API = GetAPIConnString()
 	cfg.CoreDNSAddr = GetCoreDNSAddr()
 	cfg.APIPort = GetAPIPort()