Skip to content

Commit

Permalink
Merge pull request #323 from jmpsec/cli-login
Browse files Browse the repository at this point in the history
Login to `osctrl-api` using `osctrl-cli`
  • Loading branch information
javuto authored Nov 9, 2022
2 parents 1de1c61 + 8a4ecaf commit c32a44e
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 78 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ notes.txt
# Tests
coverage.out

# Configuration
osctrl-api.json

# Go Workspace
go.work
go.work.sum
82 changes: 82 additions & 0 deletions api/handlers-login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/jmpsec/osctrl/settings"
"github.com/jmpsec/osctrl/types"
"github.com/jmpsec/osctrl/users"
"github.com/jmpsec/osctrl/utils"
)

const (
metricAPILoginReq = "login-req"
metricAPILoginErr = "login-err"
metricAPILoginOK = "login-ok"
)

// POST Handler for API login request
func apiLoginHandler(w http.ResponseWriter, r *http.Request) {
incMetric(metricAPILoginReq)
utils.DebugHTTPDump(r, settingsmgr.DebugHTTP(settings.ServiceAPI), false)
vars := mux.Vars(r)
// Extract environment
envVar, ok := vars["env"]
if !ok {
apiErrorResponse(w, "error with environment", http.StatusInternalServerError, nil)
incMetric(metricAPILoginErr)
return
}
// Get environment
env, err := envs.Get(envVar)
if err != nil {
apiErrorResponse(w, "error getting environment", http.StatusInternalServerError, nil)
incMetric(metricAPILoginErr)
return
}
var l types.ApiLoginRequest
// Parse request JSON body
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
apiErrorResponse(w, "error parsing POST body", http.StatusInternalServerError, err)
incMetric(metricAPILoginErr)
return
}
// Check credentials
access, user := apiUsers.CheckLoginCredentials(l.Username, l.Password)
if !access {
apiErrorResponse(w, "invalid credentials", http.StatusForbidden, err)
incMetric(metricAPILoginErr)
return
}
// Check if user has access to this environment
if !apiUsers.CheckPermissions(l.Username, users.AdminLevel, env.UUID) {
apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", l.Username))
incMetric(metricAPILoginErr)
return
}
// Do we have a token already?
if user.APIToken == "" {
token, exp, err := apiUsers.CreateToken(l.Username)
if err != nil {
apiErrorResponse(w, "error creating token", http.StatusInternalServerError, err)
incMetric(metricAPILoginErr)
return
}
if err = apiUsers.UpdateToken(l.Username, token, exp); err != nil {
apiErrorResponse(w, "error updating token", http.StatusInternalServerError, err)
incMetric(metricAPILoginErr)
return
}
user.APIToken = token
}
// Serialize and serve JSON
if settingsmgr.DebugService(settings.ServiceAPI) {
log.Printf("DebugService: Returning token for %s", user.Username)
}
utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, types.ApiLoginResponse{Token: user.APIToken})
incMetric(metricAPILoginOK)
}
5 changes: 5 additions & 0 deletions api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const (
apiPrefixPath = "/api"
// API version path
apiVersionPath = "/v1"
// API login path
apiLoginPath = "/login"
// API nodes path
apiNodesPath = "/nodes"
// API queries path
Expand Down Expand Up @@ -481,6 +483,9 @@ func osctrlAPIService() {
// API: forbidden
routerAPI.HandleFunc(forbiddenPath, forbiddenHTTPHandler).Methods("GET")

// ///////////////////////// UNAUTHENTICATED
routerAPI.Handle(_apiPath(apiLoginPath)+"/{env}", handlerAuthCheck(http.HandlerFunc(apiLoginHandler))).Methods("POST")
routerAPI.Handle(_apiPath(apiLoginPath)+"/{env}/", handlerAuthCheck(http.HandlerFunc(apiLoginHandler))).Methods("POST")
// ///////////////////////// AUTHENTICATED
// API: nodes by environment
routerAPI.Handle(_apiPath(apiNodesPath)+"/{env}/node/{node}", handlerAuthCheck(http.HandlerFunc(apiNodeHandler))).Methods("GET")
Expand Down
32 changes: 32 additions & 0 deletions cli/api-login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

import (
"encoding/json"
"fmt"
"strings"

"github.com/jmpsec/osctrl/types"
)

// PostLogin to login into API to retrieve a token
func (api *OsctrlAPI) PostLogin(env, username, password string) (types.ApiLoginResponse, error) {
var res types.ApiLoginResponse
l := types.ApiLoginRequest{
Username: username,
Password: password,
}
jsonMessage, err := json.Marshal(l)
if err != nil {
return res, fmt.Errorf("error marshaling data %s", err)
}
jsonParam := strings.NewReader(string(jsonMessage))
reqURL := fmt.Sprintf("%s%s%s/%s", api.Configuration.URL, APIPath, APILogin, env)
rawRes, err := api.PostGeneric(reqURL, jsonParam)
if err != nil {
return res, fmt.Errorf("error api request - %v - %s", err, string(rawRes))
}
if err := json.Unmarshal(rawRes, &res); err != nil {
return res, fmt.Errorf("can not parse body - %v", err)
}
return res, nil
}
22 changes: 21 additions & 1 deletion cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
Expand All @@ -25,6 +26,8 @@ const (
APICarves = "/carves"
// APIUsers
APIUSers = "/users"
// APILogin
APILogin = "/login"
// JSONApplication for Content-Type headers
JSONApplication = "application/json"
// JSONApplicationUTF8 for Content-Type headers, UTF charset
Expand Down Expand Up @@ -52,7 +55,7 @@ type OsctrlAPI struct {
Headers map[string]string
}

// loadAPIConfiguration to load the DB configuration file and assign to variables
// loadAPIConfiguration to load the API configuration file and assign to variables
func loadAPIConfiguration(file string) (JSONConfigurationAPI, error) {
var config JSONConfigurationAPI
// Load file and read config
Expand All @@ -72,6 +75,23 @@ func loadAPIConfiguration(file string) (JSONConfigurationAPI, error) {
return config, nil
}

// writeAPIConfiguration to write the API configuration file and update values
func writeAPIConfiguration(file string, apiConf JSONConfigurationAPI) error {
if apiConf.URL == "" || apiConf.Token == "" {
return fmt.Errorf("invalid JSON values")
}
fileData := make(map[string]JSONConfigurationAPI)
fileData[projectName] = apiConf
confByte, err := json.MarshalIndent(fileData, "", " ")
if err != nil {
return fmt.Errorf("error serializing data %s", err)
}
if err := ioutil.WriteFile(file, confByte, 0644); err != nil {
return fmt.Errorf("error writing to file %s", err)
}
return nil
}

// CreateAPI to initialize the API client and handlers
func CreateAPI(config JSONConfigurationAPI, insecure bool) *OsctrlAPI {
var a *OsctrlAPI
Expand Down
105 changes: 92 additions & 13 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/jmpsec/osctrl/types"
"github.com/jmpsec/osctrl/users"
"github.com/jmpsec/osctrl/version"
"golang.org/x/term"

"github.com/urfave/cli/v2"
)
Expand All @@ -30,6 +31,8 @@ const (
appUsage string = "CLI for " + projectName
// Application description
appDescription string = appUsage + ", a fast and efficient osquery management"
// JSON file with API token
defaultApiConfigFile = projectName + "-api.json"
)

const (
Expand Down Expand Up @@ -61,13 +64,14 @@ var (

// Variables for flags
var (
dbFlag bool
apiFlag bool
formatFlag string
silentFlag bool
insecureFlag bool
dbConfigFile string
apiConfigFile string
dbFlag bool
apiFlag bool
formatFlag string
silentFlag bool
insecureFlag bool
writeApiFileFlag bool
dbConfigFile string
apiConfigFile string
)

// Initialization code
Expand All @@ -93,7 +97,7 @@ func init() {
&cli.StringFlag{
Name: "api-file",
Aliases: []string{"A"},
Value: "",
Value: defaultApiConfigFile,
Usage: "Load API JSON configuration from `FILE`",
EnvVars: []string{"API_CONFIG_FILE"},
Destination: &apiConfigFile,
Expand Down Expand Up @@ -1442,6 +1446,29 @@ func init() {
Usage: "Checks API token",
Action: checkAPI,
},
{
Name: "login",
Usage: "Login into API and generate JSON config file with token",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "User to be used in login",
},
&cli.StringFlag{
Name: "environment",
Aliases: []string{"e"},
Usage: "Environment to be used in login",
},
&cli.BoolFlag{
Name: "write-api-file",
Aliases: []string{"w"},
Destination: &writeApiFileFlag,
Usage: "Write API configuration to JSON file",
},
},
Action: loginAPI,
},
}
// Initialize formats values
formats = make(map[string]bool)
Expand All @@ -1467,6 +1494,9 @@ func checkDB(c *cli.Context) error {
if err := db.Check(); err != nil {
return err
}
if !silentFlag {
fmt.Println("✅ DB check successful")
}
// Should be good
return nil
}
Expand All @@ -1483,6 +1513,55 @@ func checkAPI(c *cli.Context) error {
// Initialize API
osctrlAPI = CreateAPI(apiConfig, insecureFlag)
}
if !silentFlag {
fmt.Println("✅ API check successful")
}
// Should be good
return nil
}

// Action for the API login
func loginAPI(c *cli.Context) error {
// API URL can is needed
if apiConfig.URL == "" {
fmt.Println("❌ API URL is required")
os.Exit(1)
}
// Initialize API
osctrlAPI = CreateAPI(apiConfig, insecureFlag)
// We need credentials
username := c.String("username")
if username == "" {
fmt.Println("❌ username is required")
os.Exit(1)
}
env := c.String("environment")
if env == "" {
fmt.Println("❌ environment is required")
os.Exit(1)
}
fmt.Printf("\n -> Please introduce your password: ")
passwordByte, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return fmt.Errorf("error reading password %s", err)
}
fmt.Println()
apiResponse, err := osctrlAPI.PostLogin(env, username, string(passwordByte))
if err != nil {
return fmt.Errorf("error in login %s", err)
}
apiConfig.Token = apiResponse.Token
if !silentFlag {
fmt.Printf("\n✅ API Login successful: %s\n", apiResponse.Token)
}
if writeApiFileFlag {
if err := writeAPIConfiguration(apiConfigFile, apiConfig); err != nil {
return fmt.Errorf("error writing to file %s, %s", apiConfigFile, err)
}
if !silentFlag {
fmt.Printf("\n✅ API config file written: %s\n", apiConfigFile)
}
}
// Should be good
return nil
}
Expand All @@ -1492,20 +1571,20 @@ func cliWrapper(action func(*cli.Context) error) func(*cli.Context) error {
return func(c *cli.Context) error {
// Verify if format is correct
if !formats[formatFlag] {
return fmt.Errorf("invalid format %s", formatFlag)
return fmt.Errorf("invalid format %s", formatFlag)
}
// DB connection will be used
if dbFlag {
// Initialize backend
if dbConfigFile != "" {
db, err = backend.CreateDBManagerFile(dbConfigFile)
if err != nil {
return fmt.Errorf("CreateDBManagerFile - %v", err)
return fmt.Errorf("CreateDBManagerFile - %v", err)
}
} else {
db, err = backend.CreateDBManager(dbConfig)
if err != nil {
return fmt.Errorf("CreateDBManager - %v", err)
return fmt.Errorf("CreateDBManager - %v", err)
}
}
// Initialize users
Expand All @@ -1529,7 +1608,7 @@ func cliWrapper(action func(*cli.Context) error) func(*cli.Context) error {
if apiConfigFile != "" {
apiConfig, err = loadAPIConfiguration(apiConfigFile)
if err != nil {
return fmt.Errorf("loadAPIConfiguration - %v", err)
return fmt.Errorf("loadAPIConfiguration - %v", err)
}
}
// Initialize API
Expand Down Expand Up @@ -1565,6 +1644,6 @@ func main() {
app.Commands = commands
app.Action = cliAction
if err := app.Run(os.Args); err != nil {
log.Fatalf("❌ Failed to execute %v", err)
log.Fatalf("❌ Failed to execute - %v", err)
}
}
Loading

0 comments on commit c32a44e

Please sign in to comment.