Skip to content
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
33 changes: 28 additions & 5 deletions api/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
package auth

import (
"fmt"
"github.com/evanebb/gobble/api/response"
"net/http"
)

func BasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler {
// ApiBasicAuth will check the basic auth credentials sent in the request against the known users,
// and return a JSON response if authentication has failed.
func ApiBasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler {
return basicAuth(db, sendBasicAuthFailedResponse)
}

// BrowserBasicAuth will check the basic auth credentials sent in the request against the known users,
// and (re)-request basic auth credentials if authentication has failed.
func BrowserBasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler {
return basicAuth(db, requestBasicAuth)
}

// basicAuth will check the basic auth credentials sent in the request against the known users,
// and execute the passed callback if authentication has failed.
func basicAuth(db ApiUserRepository, authFailureCallback func(w http.ResponseWriter)) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok {
basicAuthFailed(w)
authFailureCallback(w)
return
}

apiUser, err := db.GetApiUserByName(user)
if err != nil {
basicAuthFailed(w)
authFailureCallback(w)
return
}

err = apiUser.CheckPassword(pass)
if err != nil {
basicAuthFailed(w)
authFailureCallback(w)
return
}

Expand All @@ -31,9 +46,17 @@ func BasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler {
}
}

func basicAuthFailed(w http.ResponseWriter) {
// sendBasicAuthFailedResponse will write a JSON response indicating authentication failure to the passed http.ResponseWriter variable.
func sendBasicAuthFailedResponse(w http.ResponseWriter) {
err := response.Error(w, http.StatusUnauthorized, "authentication failed")
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}

// sendBasicAuthFailedResponse will request basic authentication by sending the 'WWW-Authenticate' header with a 401 status code.
func requestBasicAuth(w http.ResponseWriter) {
w.Header().Set("WWW-Authenticate", `Basic realm="gobble"`)
w.WriteHeader(401)
_, _ = fmt.Fprint(w, "Unauthorised")
}
48 changes: 30 additions & 18 deletions kernelparameters/kernel_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,33 @@ import (

type KernelParameters map[string]string

func New(s []string) (KernelParameters, error) {
// String returns the string representation of the KernelParameters, e.g. 'initrd=initrd quiet splash'
func (k KernelParameters) String() string {
return strings.Join(k.StringSlice(), " ")
}

// StringSlice returns the set of KernelParameters as a slice of strings, e.g. ["initrd=initrd", "quiet", "splash"]
func (k KernelParameters) StringSlice() []string {
var s []string

for k, v := range k {
if len(v) > 0 {
s = append(s, fmt.Sprintf("%s=%s", k, v))
} else {
s = append(s, k)
}
}

return s
}

// ParseString will parse and validate s into a set of KernelParameters, and return an error if it encounters an invalid kernel parameter.
func ParseString(s string) (KernelParameters, error) {
return ParseStringSlice(strings.Split(s, " "))
}

// ParseStringSlice will parse and validate s into a set of KernelParameters, and return an error if it encounters an invalid kernel parameter.
func ParseStringSlice(s []string) (KernelParameters, error) {
kp := make(KernelParameters)

for _, v := range s {
Expand All @@ -19,10 +45,10 @@ func New(s []string) (KernelParameters, error) {

splitStr := strings.Split(v, "=")
if len(splitStr) > 1 {
// key value parameter, e.g. initrd=initrd.img
// key value parameter, e.g. 'initrd=initrd'
kp[splitStr[0]] = splitStr[1]
} else {
// just a value, e.g. noquiet
// just a value, e.g. 'quiet'
kp[splitStr[0]] = ""
}
}
Expand All @@ -43,21 +69,7 @@ func validateParameter(v string) error {
return nil
}

func FormatKernelParameters(kp KernelParameters) []string {
var s []string

for k, v := range kp {
if len(v) > 0 {
s = append(s, fmt.Sprintf("%s=%s", k, v))
} else {
s = append(s, k)
}
}

return s
}

// MergeKernelParameters merges multiple KernelParameter maps, with values from maps being overwritten in the order that they're passed into the function
// MergeKernelParameters merges multiple sets of KernelParameters, with values from maps being overwritten in the order that they're passed into the function
func MergeKernelParameters(kp1 KernelParameters, kp2 ...KernelParameters) KernelParameters {
for _, kpMap := range kp2 {
for k, v := range kpMap {
Expand Down
75 changes: 40 additions & 35 deletions kernelparameters/kernel_parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,38 @@ package kernelparameters
import (
"errors"
"reflect"
"sort"
"testing"
)

func TestNewKernelParameters(t *testing.T) {
func TestParseString(t *testing.T) {
v := "test1 test2=value"

expected := KernelParameters{
"test1": "",
"test2": "value",
}

actual, err := ParseString(v)
if !reflect.DeepEqual(actual, expected) || err != nil {
t.Fatalf(`ParseString() = %v, %v, expected: %v, nil`, actual, err, expected)
}
}

func TestParseStringInvalidParameter(t *testing.T) {
var actualErr *InvalidParameterError
expectedErr := NewInvalidParameterError("===invalid")

v := "===invalid test2=value"

expectedValue := make(KernelParameters)

actualValue, err := ParseString(v)
if err == nil || !errors.As(err, &actualErr) {
t.Fatalf(`ParseString() = %v, %v, expected: %v, %v`, actualValue, actualErr, expectedValue, expectedErr)
}
}

func TestParseStringSlice(t *testing.T) {
v := []string{
"test1",
"test2=value",
Expand All @@ -18,31 +45,26 @@ func TestNewKernelParameters(t *testing.T) {
"test2": "value",
}

actual, err := New(v)
actual, err := ParseStringSlice(v)
if !reflect.DeepEqual(actual, expected) || err != nil {
t.Fatalf(`New() = %v, %v, expected: %v, nil`, actual, err, expected)
t.Fatalf(`ParseStringSlice() = %v, %v, expected: %v, nil`, actual, err, expected)
}
}

func TestFormatKernelParameters(t *testing.T) {
v := KernelParameters{
"test1": "",
"test2": "value",
}
func TestParseStringSliceInvalidParameter(t *testing.T) {
var actualErr *InvalidParameterError
expectedErr := NewInvalidParameterError("this is invalid")

expected := []string{
"test1",
v := []string{
"this is invalid",
"test2=value",
}

actual := FormatKernelParameters(v)

// Sort the slices, since the order is not guaranteed and I don't care about it either
sort.Strings(actual)
sort.Strings(expected)
expectedValue := make(KernelParameters)

if !reflect.DeepEqual(actual, expected) {
t.Fatalf(`FormatKernelParameters() = %v, expected: %v`, actual, expected)
actualValue, err := ParseStringSlice(v)
if err == nil || !errors.As(err, &actualErr) {
t.Fatalf(`ParseStringSlice() = %v, %v, expected: %v, %v`, actualValue, actualErr, expectedValue, expectedErr)
}
}

Expand Down Expand Up @@ -70,20 +92,3 @@ func TestMergeKernelParameters(t *testing.T) {
t.Fatalf(`MergeKernelParameters() = %v, expected: %v`, actual, expected)
}
}

func TestInvalidParameterError(t *testing.T) {
var actualErr *InvalidParameterError
expectedErr := NewInvalidParameterError("this is invalid")

v := []string{
"this is invalid",
"test2=value",
}

expectedValue := make(KernelParameters)

actualValue, err := New(v)
if err == nil || !errors.As(err, &actualErr) {
t.Fatalf(`New() = %v, %v, expected: %v, %v`, actualValue, actualErr, expectedValue, expectedErr)
}
}
6 changes: 3 additions & 3 deletions repository/postgres/profile_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (r ProfileRepository) GetProfiles() ([]profile.Profile, error) {
return profiles, err
}

kp, err := kernelparameters.New(pp.KernelParameters)
kp, err := kernelparameters.ParseStringSlice(pp.KernelParameters)
if err != nil {
return profiles, err
}
Expand Down Expand Up @@ -77,7 +77,7 @@ func (r ProfileRepository) GetProfileById(id uuid.UUID) (profile.Profile, error)
}

// If this errors someone directly inserted garbage into the database :(
kp, err := kernelparameters.New(pp.KernelParameters)
kp, err := kernelparameters.ParseStringSlice(pp.KernelParameters)
if err != nil {
return pr, err
}
Expand All @@ -87,7 +87,7 @@ func (r ProfileRepository) GetProfileById(id uuid.UUID) (profile.Profile, error)

func (r ProfileRepository) SetProfile(p profile.Profile) error {
stmt := "INSERT INTO profile (uuid, name, description, kernel, initrd, kernelParameters) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (uuid) DO UPDATE set name = $2, description = $3, kernel = $4, initrd = $5, kernelParameters = $6"
_, err := r.db.Exec(context.Background(), stmt, p.Id, p.Name, p.Description, p.Kernel, p.Initrd, kernelparameters.FormatKernelParameters(p.KernelParameters))
_, err := r.db.Exec(context.Background(), stmt, p.Id, p.Name, p.Description, p.Kernel, p.Initrd, p.KernelParameters.StringSlice())
if err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions repository/postgres/system_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (r SystemRepository) GetSystems() ([]system.System, error) {
return systems, err
}

kp, err := kernelparameters.New(ps.KernelParameters)
kp, err := kernelparameters.ParseStringSlice(ps.KernelParameters)
if err != nil {
return systems, err
}
Expand Down Expand Up @@ -78,7 +78,7 @@ func (r SystemRepository) GetSystemByMacAddress(mac net.HardwareAddr) (system.Sy
}

// If this errors someone directly inserted garbage into the database :(
kp, err := kernelparameters.New(ps.KernelParameters)
kp, err := kernelparameters.ParseStringSlice(ps.KernelParameters)
if err != nil {
return sys, err
}
Expand All @@ -100,7 +100,7 @@ func (r SystemRepository) GetSystemById(id uuid.UUID) (system.System, error) {
}

// If this errors someone directly inserted garbage into the database :(
kp, err := kernelparameters.New(ps.KernelParameters)
kp, err := kernelparameters.ParseStringSlice(ps.KernelParameters)
if err != nil {
return sys, err
}
Expand All @@ -110,7 +110,7 @@ func (r SystemRepository) GetSystemById(id uuid.UUID) (system.System, error) {

func (r SystemRepository) SetSystem(s system.System) error {
stmt := "INSERT INTO system (uuid, name, description, profile, mac, kernelParameters) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (uuid) DO UPDATE set name = $2, description = $3, profile = $4, mac = $5, kernelParameters = $6"
_, err := r.db.Exec(context.Background(), stmt, s.Id, s.Name, s.Description, s.Profile, s.Mac, kernelparameters.FormatKernelParameters(s.KernelParameters))
_, err := r.db.Exec(context.Background(), stmt, s.Id, s.Name, s.Description, s.Profile, s.Mac, s.KernelParameters.StringSlice())
if err != nil {
return err
}
Expand Down
9 changes: 9 additions & 0 deletions resources/resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package resources

import "embed"

//go:embed templates
var Templates embed.FS

//go:embed static
var Static embed.FS
7 changes: 7 additions & 0 deletions resources/static/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.alternate-bg-color .row:nth-of-type(odd) {
background-color: var(--bs-card-cap-bg);
}

.table-fixed-width {
table-layout: fixed;
}
56 changes: 56 additions & 0 deletions resources/templates/base.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{{ define "base" }}
<html lang="en">
<head>
<title>Gobble - {{ .Title }}</title>
<meta charset="UTF-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous">
<link href="/ui/static/css/style.css" rel="stylesheet">
</head>
<body>
{{ if not .DisableNavbar }}
<nav class="navbar sticky-top navbar-expand-lg bg-dark border-bottom border-body" data-bs-theme="dark">
<div class="container-xxl">
<a class="navbar-brand" href="/ui/">Gobble</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Profiles
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/ui/profiles">Overview</a></li>
<li><a class="dropdown-item" href="/ui/profiles/create">Create new</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Systems
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/ui/systems">Overview</a></li>
<li><a class="dropdown-item" href="/ui/systems/create">Create new</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
{{ end }}
<main>
{{ template "content" .Data}}
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
</body>
</html>
{{ end }}
5 changes: 5 additions & 0 deletions resources/templates/error.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{ define "content" }}
<div class="container-xxl">
<p>Error occurred: {{ . }}</p>
</div>
{{ end }}
9 changes: 9 additions & 0 deletions resources/templates/errors/404.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{ define "content" }}
<div class="d-flex align-items-center justify-content-center vh-100 bg-dark">
<div class="text-center">
<h1 class="display-1 fw-bold text-white">404</h1>
<p class="fs-3 text-white">Oops! There is nothing here.</p>
<a href="/ui/" class="btn btn-light">Go to homepage</a>
</div>
</div>
{{ end }}
Loading