POC pluggable auth method

Fixes #32

The changes are as follows:
1. An example for basic auth
2. An example for cookie based auth
3. An example for JWT (oauth)
4. Update the linting tools to also test the examples dir
5. Update the examples structure to be more logical

Things still needing to be worked:
1. Determine the best way to handle confs pertaining to auth
2. Understand how credentials are going to be passed where
3. How to store user credentials

Change-Id: Ie8798131d7fa338a8aeec3303593afb0390ab393
This commit is contained in:
Schiefelbein, Andrew 2020-05-04 12:57:28 -05:00
parent c4fb668985
commit dc43d5b17d
12 changed files with 589 additions and 156 deletions

View File

@ -36,9 +36,13 @@ run:
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# skip-dirs:
# - src/external_libs
# - autogenerated_by_my_lib
skip-dirs-use-default: false
skip-dirs:
- bin$
- docs$
- playbooks$
- tools$
- web$
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is

View File

@ -19,8 +19,8 @@ GO_FLAGS := -ldflags=$(LD_FLAGS)
BUILD_DIR := bin
# Find all main.go files under cmd, excluding airshipui itself (which is the octant wrapper)
EXAMPLE_PLUGIN_NAMES := $(shell basename $(subst /main.go,,$(shell find examples/plugins -name "main.go")))
EXAMPLE_PLUGINS := $(addprefix $(BUILD_DIR)/, $(EXAMPLE_PLUGIN_NAMES))
EXAMPLE_NAMES := $(shell basename $(subst /main.go,,$(shell find examples -name "main.go")))
EXAMPLES := $(addprefix $(BUILD_DIR)/, $(EXAMPLE_NAMES))
MAIN := $(BUILD_DIR)/airshipui
EXTENSION :=
@ -38,19 +38,19 @@ DIRS = internal
RECURSIVE_DIRS = $(addprefix ./, $(addsuffix /..., $(DIRS)))
.PHONY: build
build: $(MAIN) $(EXAMPLE_PLUGINS)
build: $(MAIN) $(EXAMPLES)
$(MAIN): FORCE
@mkdir -p $(BUILD_DIR)
go build -o $(MAIN)$(EXTENSION) $(GO_FLAGS) cmd/$(@F)/main.go
$(EXAMPLE_PLUGINS): FORCE
$(EXAMPLES): FORCE
@mkdir -p $(BUILD_DIR)
go build -o $@$(EXTENSION) $(GO_FLAGS) examples/plugins/$(@F)/main.go
go build -o $@$(EXTENSION) $(GO_FLAGS) examples/$(@F)/main.go
FORCE:
.PHONY: install-octant-plugins
install-octant-plugins: $(EXAMPLE_PLUGINS)
install-octant-plugins: $(EXAMPLES)
@mkdir -p $(OCTANT_PLUGINSTUB_DIR)
cp $(addsuffix $(EXTENSION), $^) $(OCTANT_PLUGINSTUB_DIR)

View File

@ -21,11 +21,37 @@ Run the airshipui binary
./bin/airshipui
# Authentication
## Pluggable authentication methods
The AirshipUI is not designed to create authentication credentials but to have them supplied to it either by a configuration or by an external entity. The expectation is that there will be an external URL that will handle authentication for the system which may need to be modified or created. The endpoint will need to be able to forward a [bearer token](https://oauth.net/2/bearer-tokens/), [basic auth](https://en.wikipedia.org/wiki/Basic_access_authentication) or cookie data to the Airship UI backend service.
To configure the pluggable authentication the following must be added to the $HOME/.airshipui/airshipui.json file:
```
"authMethod": {
"url": "<protocol>://<host:port>/<path>/<method>"
}
```
Note: By default the system will start correctly without any authentication urls supplied to the configuration. The expectation is that AirshipUI will be running in a minimal least authorized configuration.
## Example Auth Server
There is an example authentication server in examples/authentication/main.go. These endpoints can be added to the $HOME/.airshipui/airshipui.json and will allow the system to show a basic authentication test.
1. Basic auth on http://127.0.0.1:12321/basic-auth
2. Cookie based auth on http://127.0.0.1:12321/cookie
3. OAuth JWT (JSON Web Token) on http://127.0.0.1:12321/oauth
To start the system cd to the root of the AirshipUI repository and execute:
```
go run examples/authentication/main.go
```
### Example Auth Server Credentials
+ The example auth server id is: airshipui
+ The example auth server password is: Open Sesame!
# Plugins
## Octant
[Octant](https://github.com/vmware-tanzu/octant) is a tool for developers to understand how applications run on a Kubernetes cluster. It aims to be part of the developer's toolkit for gaining insight and approaching complexity found in Kubernetes. Octant offers a combination of introspective tooling, cluster navigation, and object management along with a plugin system to further extend its capabilities.
Octant needs to be pointed to a Kubernetes Cluster. For development we recommend [setting up Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/)
Octant needs to be pointed to a Kubernetes Cluster. For development it is recommended to use [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/)
### How to get and build Octant
If you are going to do serious Octant development you will need to adhere to [Octant's Hacking Guide](https://github.com/vmware-tanzu/octant/blob/master/HACKING.md) which includes information on how to build Octant and the steps to push changes to them.

264
examples/authentication/main.go Executable file
View File

@ -0,0 +1,264 @@
/*
Copyright (c) 2020 AT&T. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
"text/template"
"time"
"github.com/dgrijalva/jwt-go"
)
// page struct is used for templated HTML
type page struct {
Title string
}
// id and password passed from the test page
type authRequest struct {
ID string `json:"id,omitempty"`
Password string `json:"password,omitempty"`
}
func main() {
// we're not picky, so we'll take everything and sort it out later
http.HandleFunc("/", handler)
log.Println("Example Auth Server listening on :12321")
err := http.ListenAndServe(":12321", nil)
if err != nil {
log.Fatal(err)
}
}
// URI check for /basic-auth, /cookie and /oauth, everything else gets a 404
// Also a switch for GET and POST, everything else gets a 415
func handler(w http.ResponseWriter, r *http.Request) {
method := r.Method
uri := r.RequestURI
if uri == "/basic-auth" || uri == "/cookie" || uri == "/oauth" {
switch method {
case http.MethodGet:
get(uri, w)
case http.MethodPost:
post(uri, w, r)
default:
w.WriteHeader(http.StatusNotFound)
log.Printf("Method %s for %s being rejected, not implemented", method, uri)
}
} else {
w.WriteHeader(http.StatusNotFound)
log.Printf("URI %s being rejected, not found", uri)
}
}
// handle the GET function and return a templated page
func get(uri string, w http.ResponseWriter) {
var p page
switch uri {
case "/basic-auth":
p = page{
Title: "Basic Auth",
}
case "/cookie":
p = page{
Title: "Cookie",
}
case "/oauth":
p = page{
Title: "OAuth",
}
}
if p != (page{}) {
// parse and merge the template
err := template.Must(template.ParseFiles("./examples/authentication/templates/index.html")).Execute(w, p)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
log.Printf("Error getting the templated html: %v", err)
http.Error(w, "Error getting the templated html", http.StatusInternalServerError)
}
}
}
// handle the POST function and return a mock authentication
func post(uri string, w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
http.Error(w, "can't read body", http.StatusBadRequest)
return
}
var authAttempt authRequest
err = json.Unmarshal(body, &authAttempt)
if err == nil {
// TODO: make the id and password part of a conf file somewhere
id := authAttempt.ID
passwd := authAttempt.Password
if id == "airshipui" && passwd == "Open Sesame!" {
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"id": id,
"name": "Some Name",
"expiration": time.Now().Add(time.Hour * 24).Unix(),
}
switch uri {
case "/basic-auth":
response["X-Auth-Token"] = base64.StdEncoding.EncodeToString([]byte(id + ":" + passwd))
response["type"] = "basic-auth"
postHelper(response, w)
case "/cookie":
response["type"] = "cookie"
cookieHandler(response, w)
case "/oauth":
response["type"] = "oauth"
jwtHandler(id, passwd, response, w)
}
} else {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
http.Error(w, "Bad id or password", http.StatusUnauthorized)
}
} else {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
log.Printf("Error unmarshalling the request: %v", err)
http.Error(w, "Error unmarshalling the request", http.StatusBadRequest)
}
}
// potentially more complex logic happens here with cookie data
func cookieHandler(response map[string]interface{}, w http.ResponseWriter) {
cookie, err := json.Marshal(response)
if err != nil {
log.Printf("Error marshaling cookie response: %v", err)
}
b, err := encrypt(cookie)
if err != nil {
log.Printf("Error encrypting cookie response: %v", err)
postHelper(nil, w)
} else {
response["cookie"] = b
postHelper(response, w)
}
}
// potentially more complex logic happens here with JWT data
func jwtHandler(id string, passwd string, response map[string]interface{}, w http.ResponseWriter) {
token, err := createToken(id, passwd)
if err != nil {
log.Printf("Error creating JWT token: %v", err)
postHelper(nil, w)
} else {
response["jwt"] = token
postHelper(response, w)
}
}
// Helper function to reduce the number of error checks that have to happen in other functions
func postHelper(returnData map[string]interface{}, w http.ResponseWriter) {
if returnData == nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
} else {
log.Printf("Auth data %s\n", returnData)
b, err := json.Marshal(returnData)
if err != nil {
log.Printf("Error marshaling the response: %v", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
} else {
_, err := w.Write(b)
if err != nil {
log.Printf("Error sending POST response to client: %v", err)
} else {
go notifyElectron(b)
}
}
}
}
// This is intended to send an auth completed message to the system so that it knows there was a successful login
func notifyElectron(data []byte) {
// TODO: probably need to pull the electron url out into its own
resp, err := http.Post("http://localhost:8080/auth", "application/json; charset=UTF-8", bytes.NewBuffer(data))
if err != nil {
log.Printf("Error sending auth complete to electron. The response is %v, the error is %v\n", resp, err)
}
}
// aes requires a 32 byte key, this is random for demo purposes
func randBytes(length int) ([]byte, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return nil, err
} else {
return b, nil
}
}
// this creates a random ciphertext for demo purposes
// this is not intended to be reverseable or to be used in production
func encrypt(data []byte) ([]byte, error) {
b, err := randBytes(256 / 8)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(b)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}
// create a JWT (JSON Web Token) for demo purposes, this is not to be used in production
func createToken(id string, passwd string) (string, error) {
// create the token
token := jwt.New(jwt.SigningMethodHS256)
// set some claims
claims := make(jwt.MapClaims)
claims["username"] = id
claims["password"] = passwd
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
token.Claims = claims
//Sign and get the complete encoded token as string
return (token.SignedString([]byte("airshipui")))
}

View File

@ -0,0 +1,86 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>AirshipUI Test {{.Title}}</title>
<link rel="icon" href="data:;base64,=">
</head>
<script>
function testIt() {
document.getElementById("AuthBtn").disabled = true;
console.log(window.location.pathname);
let xhr = new XMLHttpRequest();
xhr.open("POST", window.location.pathname);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onload = function () {
if (this.status === 201) {
document.cookie = "airshipUI=" + xhr.response + "expires=" + new Date().getUTCDate;
console.log(JSON.parse(xhr.response));
} else {
console.log({
status: this.status,
statusText: xhr.statusText
});
document.getElementById("OutputDiv").innerHTML = '<span style="color:red"><h2>&#9760; ID or Password is incorrect please try again &#9760;</h2></span>';
document.getElementById("AuthBtn").disabled = false;
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
let json = JSON.stringify({"id": document.getElementById("ID").value, "password": document.getElementById("Passwd").value});
console.log(json)
xhr.send(json);
}
</script>
<body>
<h1>Airship UI Test {{.Title}}</h1>
<table>
<tr>
<td>
<b>Id:</b>&nbsp;&nbsp;
</td>
<td>
<input type="text" id="ID">
</td>
</tr>
<tr>
<td>
<b>Password:</b>&nbsp;&nbsp;
</td>
<td>
<input type="password" id="Passwd">
</td>
</tr>
<tr>
<td colspan="2">
<button id="AuthBtn" onclick="testIt()">Test It!</button>
</td>
</tr>
</table>
<div id="OutputDiv"></div>
<h2>&#9888; Warning! &#9888;</h2>
<p>This is a {{.Title}} test page is only intended as an example for how to use {{.Title}} with AirshipUI.</p>
<p>The System will return the following HTML status codes and responses</p>
<ul>
{{if eq .Title "Basic Auth"}}
<li>201: Created. The password attempt was successful and the backend has sent an xauth token header to AirshipUI.</li>
{{else if eq .Title "Cookie"}}
<li>201: Created. The password attempt was successful and the backend has set a cookie and sent the cookie contents to AirshipUI.</li>
{{else if eq .Title "OAuth"}}
<li>201: Created. The password attempt was successful and the backend has set a JWT (JSON Web Token) and sent the JWT contents to AirshipUI.</li>
{{end}}
<li>400: Bad request. There was an error sending the system the authentication request, most likely bad JSON.</li>
<li>401: Unauthorized. Bad id / password attempt.</li>
<li>403: Forbidden. The id / password combination was correct but the id is not allowed for the resource.</li>
<li>500: Internal Server Error. There was a processing error on the back end.</li>
</ul>
</body>
</html>

View File

@ -1,86 +1,86 @@
/*
Copyright (c) 2020 AT&T. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"log"
"github.com/vmware-tanzu/octant/pkg/navigation"
"github.com/vmware-tanzu/octant/pkg/plugin"
"github.com/vmware-tanzu/octant/pkg/plugin/service"
"github.com/vmware-tanzu/octant/pkg/view/component"
)
var pluginName = "airshipui-example-plugin"
// HelloWorldPlugin is a required struct to be an octant compliant plugin
type HelloWorldPlugin struct{}
// return a new hello world struct
func newHelloWorldPlugin() *HelloWorldPlugin {
return &HelloWorldPlugin{}
}
// This is a sample plugin showing the features of Octant's plugin API.
func main() {
// Remove the prefix from the go logger since Octant will print logs with timestamps.
log.SetPrefix("")
// Tell Octant to call this plugin when printing configuration or tabs for Pods
capabilities := &plugin.Capabilities{
IsModule: true,
}
hwp := newHelloWorldPlugin()
// Set up what should happen when Octant calls this plugin.
options := []service.PluginOption{
service.WithNavigation(hwp.handleNavigation, hwp.initRoutes),
}
// Use the plugin service helper to register this plugin.
p, err := service.Register(pluginName, "The very smallest thing you can do", capabilities, options...)
if err != nil {
log.Fatal(err)
}
// The plugin can log and the log messages will show up in Octant.
log.Printf("hello-world-plugin is starting")
p.Serve()
}
// handles the navigation pane interation
func (hwp *HelloWorldPlugin) handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) {
return navigation.Navigation{
Title: "Hello World Plugin",
Path: request.GeneratePath(),
IconName: "folder",
}, nil
}
// initRoutes routes for this plugin. In this example, there is a global catch all route
// that will return the content for every single path.
func (hwp *HelloWorldPlugin) initRoutes(router *service.Router) {
router.HandleFunc("*", hwp.routeHandler)
}
// this function returns the octant wrapped HTML content for the page
func (hwp *HelloWorldPlugin) routeHandler(request service.Request) (component.ContentResponse, error) {
contentResponse := component.NewContentResponse(component.TitleFromString("Hello World Title"))
helloWorld := component.NewText("Hello World just some text on the page")
contentResponse.Add(helloWorld)
return *contentResponse, nil
}
/*
Copyright (c) 2020 AT&T. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"log"
"github.com/vmware-tanzu/octant/pkg/navigation"
"github.com/vmware-tanzu/octant/pkg/plugin"
"github.com/vmware-tanzu/octant/pkg/plugin/service"
"github.com/vmware-tanzu/octant/pkg/view/component"
)
var pluginName = "airshipui-example-plugin"
// HelloWorldPlugin is a required struct to be an octant compliant plugin
type HelloWorldPlugin struct{}
// return a new hello world struct
func newHelloWorldPlugin() *HelloWorldPlugin {
return &HelloWorldPlugin{}
}
// This is a sample plugin showing the features of Octant's plugin API.
func main() {
// Remove the prefix from the go logger since Octant will print logs with timestamps.
log.SetPrefix("")
// Tell Octant to call this plugin when printing configuration or tabs for Pods
capabilities := &plugin.Capabilities{
IsModule: true,
}
hwp := newHelloWorldPlugin()
// Set up what should happen when Octant calls this plugin.
options := []service.PluginOption{
service.WithNavigation(hwp.handleNavigation, hwp.initRoutes),
}
// Use the plugin service helper to register this plugin.
p, err := service.Register(pluginName, "The very smallest thing you can do", capabilities, options...)
if err != nil {
log.Fatal(err)
}
// The plugin can log and the log messages will show up in Octant.
log.Printf("hello-world-plugin is starting")
p.Serve()
}
// handles the navigation pane interation
func (hwp *HelloWorldPlugin) handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) {
return navigation.Navigation{
Title: "Hello World Plugin",
Path: request.GeneratePath(),
IconName: "folder",
}, nil
}
// initRoutes routes for this plugin. In this example, there is a global catch all route
// that will return the content for every single path.
func (hwp *HelloWorldPlugin) initRoutes(router *service.Router) {
router.HandleFunc("*", hwp.routeHandler)
}
// this function returns the octant wrapped HTML content for the page
func (hwp *HelloWorldPlugin) routeHandler(request service.Request) (component.ContentResponse, error) {
contentResponse := component.NewContentResponse(component.TitleFromString("Hello World Title"))
helloWorld := component.NewText("Hello World just some text on the page")
contentResponse.Add(helloWorld)
return *contentResponse, nil
}

3
go.mod
View File

@ -3,6 +3,7 @@ module opendev.org/airship/airshipui
go 1.13
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gophercloud/gophercloud v0.9.0
github.com/gorilla/websocket v1.4.2
github.com/spf13/cobra v0.0.6
@ -14,4 +15,4 @@ require (
opendev.org/airship/airshipctl v0.0.0-20200324160507-db6217f011b9
)
replace k8s.io/client-go => k8s.io/client-go v0.0.0-20191114101535-6c5935290e33
replace k8s.io/client-go => k8s.io/client-go v0.0.0-20191114101535-6c5935290e33

View File

@ -13,47 +13,42 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
package webservice
package configs
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
)
// basic structure for a given external dashboard
// AirshipuiProps basic structure for a given external dashboard
// TODO: solidify the struct requirements for the input
type extPlugins struct {
// TODO: maybe move where props gathering and parsing lives
type AirshipuiProps struct {
AuthMethod struct {
Type string `json:"type,omitempty"`
Value []string `json:"values,omitempty"`
URL string `json:"url,omitempty"`
} `json:"authMethod"`
ExtDashboard []interface{} `json:"external_dashboards"`
}
// cache the file so we don't have to reread every execution
var pluginCache map[string]interface{}
// getPlugins updates the pluginCache from file if needed
func getPlugins() map[string]interface{} {
if pluginCache == nil {
err := getPluginsFromFile()
if err != nil {
log.Printf("Error attempting to get plugins from file: %s\n", err)
}
}
return pluginCache
}
// AirshipuiPropsCache the file so we don't have to reread every execution
// TODO: maybe move where props gathering and parsing lives
var AirshipuiPropsCache AirshipuiProps
// TODO: add watcher to the json file to reload conf on change
// Get dashboard links for Plugins if present in $HOME/.airshipui/plugins.json
func getPluginsFromFile() error {
// TODO: maybe move where props gathering and parsing lives
// Get dashboard info if present in $HOME/.airshipui/airshipui.json
func GetConfsFromFile() error {
var fileName string
home, err := os.UserHomeDir()
if err != nil {
log.Printf("Error determining the home directory %s\n", err)
return err
}
fileName = filepath.FromSlash(home + "/.airship/plugins.json")
fileName = filepath.FromSlash(home + "/.airship/airshipui.json")
jsonFile, err := os.Open(fileName)
if err != nil {
@ -68,20 +63,10 @@ func getPluginsFromFile() error {
return err
}
var plugins extPlugins
err = json.Unmarshal(byteValue, &plugins)
err = json.Unmarshal(byteValue, &AirshipuiPropsCache)
if err != nil {
return err
}
log.Printf("Plugins found: %v\n", plugins)
pluginCache = map[string]interface{}{
"type": "plugins",
"component": "dropdown",
"timestamp": time.Now().UnixNano() / 1000000,
"plugins": plugins,
}
return err
}

View File

@ -22,6 +22,7 @@ import (
"time"
"github.com/gorilla/websocket"
"opendev.org/airship/airshipui/internal/configs"
)
// just a base structure to return from the web service
@ -43,16 +44,17 @@ var upgrader = websocket.Upgrader{
// TODO: make this a dynamic registration of components
var functionMap = map[string]map[string]func() map[string]interface{}{
"electron": {
"keepalive": keepaliveReply,
"getID": keepaliveReply,
},
"initialize": {
"getAll": getPlugins,
"keepalive": keepaliveReply,
"initialize": clientInit,
},
}
// websocket that'll be reused by several places
var ws *websocket.Conn
// semaphore to signal the UI to authenticate
var isAuthenticated bool
// handle the origin request & upgrade to websocket
func onOpen(w http.ResponseWriter, r *http.Request) {
// gorilla ws will give a 403 on a cross origin request, so we silence its complaints
@ -133,8 +135,36 @@ func onError(err error) {
log.Printf("Error receiving / sending message: %s\n", err)
}
// WebServer will run the handler functions for both normal REST requests and WebSockets
// handle an auth complete attempt
func handleAuth(w http.ResponseWriter, r *http.Request) {
// TODO: handle the response body to capture the credentials
err := ws.WriteJSON(map[string]interface{}{
"type": "electron",
"component": "authcomplete",
"timestamp": time.Now().UnixNano() / 1000000,
})
// error sending the websocket request
if err != nil {
onError(err)
} else {
isAuthenticated = true
}
}
// WebServer will run the handler functions for WebSockets
// TODO: potentially add in the ability to serve static content
func WebServer() {
// TODO: maybe move where props gathering and parsing lives
err := configs.GetConfsFromFile()
if err != nil {
log.Fatalf("Error getting data from the config file: %s\n", err)
}
// some things may need a redirect so we'll give them a url to do that with
http.HandleFunc("/auth", handleAuth)
// hand off the websocket upgrade over http
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
onOpen(w, r)
})
@ -144,3 +174,19 @@ func WebServer() {
log.Fatal("ListenAndServe:", err)
}
}
func clientInit() map[string]interface{} {
// if no auth method is supplied start with minimal functionality
if len(configs.AirshipuiPropsCache.AuthMethod.URL) == 0 {
isAuthenticated = true
}
return map[string]interface{}{
"type": "electron",
"component": "initialize",
"timestamp": time.Now().UnixNano() / 1000000,
"isAuthenticated": isAuthenticated,
"plugins": configs.AirshipuiPropsCache.ExtDashboard,
"authentication": configs.AirshipuiPropsCache.AuthMethod,
}
}

View File

@ -13,7 +13,7 @@
</head>
<body>
<div id="HeaderDiv" class="topnav">
<div id="HeaderDiv" class="topnav" style="display:none">
<div id="HeaderNameDiv" class="topnavsuite"></div>
<a href="index.html" id="AirshipUIHome">Airship UI</a>
<div class="dropdown">
@ -26,7 +26,7 @@
<div id="dashboard">
<webview class="webview" id="DashView" autosize="on" style="height:92vh;display:none"></webview>
</div>
<div id="FooterDiv">
<div id="FooterDiv" style="display:none">
</br>
<center><img src="images/airship-color.png" width="100px" height="67px"></center>
<center>

View File

@ -13,9 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
const remote = require('electron').remote;
const app = remote.app;
const config = require('electron-json-config')
// add the footer and header when the page loads
if (document.addEventListener) {
@ -35,10 +32,9 @@ if (document.addEventListener) {
// add dashboard links to Plugins if present in $HOME/.airshipui/plugins.json
function addPlugins(json) {
console.log(json);
let dropdown = document.getElementById("PluginDropdown");
for (let i = 0; i < json.external_dashboards.length; i++) {
let dash = json.external_dashboards[i];
for (let i = 0; i < json.length; i++) {
let dash = json[i];
let a = document.createElement("a");
a.innerText = dash["name"];
@ -56,6 +52,15 @@ function addPlugins(json) {
}
}
function authenticate(json) {
// use webview to display the auth page
let view = document.getElementById("DashView");
view.src = json["url"];
document.getElementById("MainDiv").style.display = 'none';
document.getElementById("DashView").style.display = '';
}
function removeElement(id) {
if (document.contains(document.getElementById(id))) {
document.getElementById(id).remove();

View File

@ -18,9 +18,9 @@ var timeout = null;
// establish a session when browser is open
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("DOMContentLoaded", function () {
// register the webservice so it's available the entire time of the process
register();
register();
}, false);
}
@ -59,15 +59,24 @@ function register() {
function handleMessages(message) {
var json = JSON.parse(message.data);
if (json["type"] === "plugins" && json["component"] === "dropdown") {
addPlugins(json["plugins"]);
// keepalives and inits aren't interesting to other pages
if (json["type"] === "electron") {
console.log(json);
if (json["component"] === "initialize") {
if (!json["isAuthenticated"]) {
authenticate(json["authentication"]);
} else {
authComplete();
}
addPlugins(json["plugins"]);
} else if (json["component"] === "authcomplete") {
authComplete();
}
} else {
// TODO: determine if we're dispatching events or just doing function calls
// events based on the type are interesting to other pages
// create and dispatch an event based on the data received
// keepalives aren't interesting so suppressing normal actions for it
if (json["electron"] !== "plugins" && json["component"] !== "keepalive") {
document.dispatchEvent(new CustomEvent(json["type"], {detail: json}));
}
document.dispatchEvent(new CustomEvent(json["type"], { detail: json }));
}
// TODO: Determine if these should be suppressed or only allowed in specific cases
@ -76,7 +85,7 @@ function handleMessages(message) {
function open() {
console.log("Websocket established");
var json = {"type":"initialize","component":"getAll"};
var json = { "type": "electron", "component": "initialize" };
ws.send(JSON.stringify(json));
// start up the keepalive so the websocket stays open
keepAlive();
@ -99,19 +108,26 @@ function close(code) {
case 1015: console.log("Web Socket Closed: closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified): ", code); break;
default: console.log("Web Socket Closed: unknown error code: ", code); break;
}
ws = null;
}
function authComplete() {
document.getElementById("HeaderDiv").style.display = '';
document.getElementById("MainDiv").style.display = '';
document.getElementById("DashView").style.display = 'none';
document.getElementById("FooterDiv").style.display = '';
}
function keepAlive() {
if (ws !== null) {
if (ws.readyState !== ws.CLOSED) {
// clear the previously set timeout
window.clearTimeout(timeout);
window.clearInterval(timeout);
var json = {"id":"poc","type":"electron","component":"keepalive"};
var json = { "id": "poc", "type": "electron", "component": "keepalive" };
ws.send(JSON.stringify(json));
timeout = window.setTimeout(keepAlive,60000);
timeout = window.setTimeout(keepAlive, 60000);
}
}
}