Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication to the Web UI #3711

Merged
merged 3 commits into from
Mar 28, 2024
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
2 changes: 0 additions & 2 deletions pkg/publicapi/endpoint/orchestrator/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
"github.com/bacalhau-project/bacalhau/pkg/publicapi/middleware"
"github.com/labstack/echo/v4"
echo_middleware "github.com/labstack/echo/v4/middleware"
)

type EndpointParams struct {
Expand Down Expand Up @@ -34,7 +33,6 @@ func NewEndpoint(params EndpointParams) *Endpoint {
// JSON group
g := e.router.Group("/api/v1/orchestrator")
g.Use(middleware.SetContentType(echo.MIMEApplicationJSON))
g.Use(echo_middleware.CORS())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we need to remove this? I don't have a problem with the change - just curious

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

g.PUT("/jobs", e.putJob)
g.POST("/jobs", e.putJob)
g.GET("/jobs", e.listJobs)
Expand Down
2 changes: 0 additions & 2 deletions pkg/publicapi/endpoint/requester/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/bacalhau-project/bacalhau/pkg/requester"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
echo_middleware "github.com/labstack/echo/v4/middleware"
)

type EndpointParams struct {
Expand Down Expand Up @@ -44,7 +43,6 @@ func NewEndpoint(params EndpointParams) *Endpoint {

g := e.router.Group("/api/v1/requester")
g.Use(middleware.SetContentType(echo.MIMEApplicationJSON))
g.Use(echo_middleware.CORS())
g.POST("/list", e.list)
g.GET("/nodes", e.nodes)
g.POST("/states", e.states)
Expand Down
1 change: 1 addition & 0 deletions pkg/publicapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func NewAPIServer(params ServerParams) (*Server, error) {
middlewareLogger := log.Ctx(logger.ContextWithNodeIDLogger(context.Background(), params.HostID))
// base middle after routing
server.Router.Use(
echomiddelware.CORS(),
echomiddelware.Recover(),
echomiddelware.RequestID(),
echomiddelware.BodyLimit(server.config.MaxBytesToReadInBody),
Expand Down
2 changes: 2 additions & 0 deletions webui/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.1.1.cjs
2 changes: 1 addition & 1 deletion webui/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = {
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/tests/mocks/fileMock.js",
"\\.(css|less)$": "<rootDir>/tests/mocks/styleMock.js",
"\\.(css|less)$": "<rootDir>/tests/mocks/styleMock.ts",
"^@pages/(.*)$": "<rootDir>/src/pages/$1",
"^@components/(.*)$": "<rootDir>/src/components/$1",
"\\.svg$": "<rootDir>/tests/mocks/svgMock.mjs",
Expand Down
4 changes: 3 additions & 1 deletion webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@jest/globals": "^29.7.0",
"@testing-library/jest-dom": "^6.4.1",
"@testing-library/react": "^14.2.1",
"@types/json-schema": "^7.0.15",
"@types/node": "^20.11.16",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
Expand All @@ -35,6 +36,7 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.0",
"react-svg": "^16.1.33",
"react-toastify": "^10.0.5",
"sass": "^1.70.0",
"semver": "^7.5.4",
"typescript": "^5.3.3",
Expand Down Expand Up @@ -108,5 +110,5 @@
"description": "WebUI for Bacalhau",
"main": "./src/index.tsx",
"homepage": ".",
"packageManager": "yarn@4.1.0"
"packageManager": "yarn@4.1.1"
}
7 changes: 6 additions & 1 deletion webui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import React from "react"
import { ToastContainer } from "react-toastify"
import 'react-toastify/dist/ReactToastify.css';
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
import { TableSettingsContextProvider } from "./context/TableSettingsContext"
import { Home } from "./pages/Home/Home"
import { JobsDashboard } from "./pages/JobsDashboard/JobsDashboard"
import { NodesDashboard } from "./pages/NodesDashboard/NodesDashboard"
import { Settings } from "./pages/Settings/Settings"
import { JobDetail } from "./pages/JobDetail/JobDetail"
import { Flow } from "./pages/Auth/Flow";

const App = () => (
<TableSettingsContextProvider>
<ToastContainer position="top-center"/>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/JobsDashboard" element={<JobsDashboard />} />
<Route path="/JobDetail/:jobId" element={<JobDetail />} />
<Route path="/NodesDashboard" element={<NodesDashboard />} />
<Route path="/Auth" element={<Flow />} />
<Route path="/Settings" element={<Settings />} />
<Route path="/JobDetail/:jobId" element={<JobDetail />} />
</Routes>
</Router>
</TableSettingsContextProvider>
Expand Down
25 changes: 2 additions & 23 deletions webui/src/components/ActionButton/ActionButton.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "../../styles/variables";
@import "../../styles/button";

.column {
display: flex;
Expand All @@ -9,29 +10,7 @@
}

.actionButton {
background-color: rgba($light-blue, 0.5);
color: $text-color-dark;
padding: 5px 15px;
border-radius: 20px;
border: none;
font-size: 14px;
cursor: pointer;
display: flex;
column-gap: 1ch;
align-items: center;

&:hover {
background-color: rgba($light-blue, 0.7);
}
}

.viewIcon {
width: 18px;
height: 18px;
}

.actionButton:active {
background-color: rgba($light-blue, 0.7);
@include button;
}

.viewIcon {
Expand Down
61 changes: 61 additions & 0 deletions webui/src/helpers/authInterfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { JSONSchema7 } from "json-schema";
import { ListRequest, ListResponse } from "./baseInterfaces";

// A request to list the authentication methods that the node supports.
export interface ListAuthnMethodsRequest extends ListRequest { }

// A response listing the authentication methods that the node supports.
export interface ListAuthnMethodsResponse extends ListResponse {
// The name of a method mapped to that method's requirements.
// The name must be subsequently used to submit data.
Methods: {[key: string]: Requirement}
}

// A requirement of an authn method, with the params varying per-type.
export type Requirement = {
type: "challenge"
params: ChallengeRequirement
} | {
type: "ask"
params: AskRequirement
}

// The "ask" type just gives the client a JSON Schema that describes the fields
// it needs to collect from the user and submit.
export type AskRequirement = JSONSchema7

// The "challenge" type gives the client an input phrase it must sign using its
// private key.
export interface ChallengeRequirement {
InputPhrase: string
}

// A request to authenticate using a given method, including any credentials.
export interface AuthnRequest {
Name: string
MethodData: AskRequest | ChallengeRequest
}

// The "ask" type needs the fields requested from the user by the JSON Schema.
export type AskRequest = {[key: string]: string}

// The "challenge" type needs the signature of the phrase and the associated
// public key.
export interface ChallengeRequest {
PhraseSignature: string
PublicKey: string
}

export interface AuthnResponse {
Authentication: Authentication
}

// A response from trying to authenticate.
export interface Authentication {
// Whether the authentication was successful.
success: boolean
// Any additional info about why authentication was successful or not.
reason?: string
// The token the client should use in subsequent API requests.
token?: string
}
18 changes: 18 additions & 0 deletions webui/src/helpers/baseInterfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// The interfaces in this file match those in base_requests.go

export interface Request {
namespace?: string
}

export interface GetRequest extends Request {}

export interface ListRequest extends GetRequest {
limit?: number
next_token?: string
order_by?: string
reverse?: boolean
}

export interface ListResponse {
NextToken: string
}
6 changes: 4 additions & 2 deletions webui/src/helpers/nodeInterfaces.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// src/interfaces.ts

import { ListRequest } from "./baseInterfaces"

export interface NodesResponse {
NextToken: string
Nodes: Node[]
}

export interface Node {
PeerInfo: PeerInfo
NodeID: string
NodeType: string
Labels: Labels
ComputeNodeInfo: ComputeNodeInfo
Expand Down Expand Up @@ -65,6 +67,6 @@ export interface ParsedNodeData {
// action: string;
}

export interface NodeListRequest {
export interface NodeListRequest extends ListRequest {
labels: string | undefined
}
20 changes: 20 additions & 0 deletions webui/src/pages/Auth/AskInput.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
form {
display: grid;
grid-template-columns: 4fr 6fr;
row-gap: 1em;
column-gap: 1em;

h3 {
margin: 0;
grid-column: span 2;
}

label {
text-transform: capitalize;
}

label[data-required=true]::after {
content: "*";
color: red;
}
}
74 changes: 74 additions & 0 deletions webui/src/pages/Auth/AskInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from "react";
import { JSONSchema7, JSONSchema7Definition } from "json-schema";
import { AskRequest, AskRequirement, AuthnRequest } from "../../helpers/authInterfaces";
import "./AskInput.module.scss"

interface AskInputProps {
name: string
requirement: AskRequirement
authenticate: (req: AuthnRequest) => void
cancel: () => void
}

function propertyToInputType(property: JSONSchema7Definition): React.HTMLInputTypeAttribute {
if (typeof property === "boolean") {
return "text"
}

if (property.writeOnly) {
return "password"
}

switch (property.type) {
case "number":
case "integer":
return "number"
case "boolean":
return "checkbox"
default:
return "text"
}
}

export const AskInput: React.FC<AskInputProps> = (props: AskInputProps) => {
const requirement = props.requirement
const properties = requirement.properties ?? {}
const fields = Object.keys(properties)
const required = requirement.required ?? []

// Sort by fields listed in required order
fields.sort((a, b) => required.indexOf(a) - required.indexOf(b))

const inputs = fields.map(field => {
const property = properties[field] as JSONSchema7
return <>
<label htmlFor={field} data-required={required.includes(field)}>
{field}
</label>
<input
name={field}
type={propertyToInputType(property)}
required={required.includes(field)} />
</>
})

const submit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault()

const formData = new FormData(event.currentTarget)
const objectData: AskRequest = {}
formData.forEach((value, key) => {objectData[key] = value.toString()})

const request: AuthnRequest = {Name: props.name, MethodData: objectData}
props.authenticate(request)

return false
}

return <form onSubmit={submit} onReset={props.cancel}>
<h3>Authenticate using {props.name.replaceAll(/[^A-Za-z0-9]/g, ' ')}</h3>
{...inputs}
<input type="reset" value="Cancel"/>
<input type="submit" value="Authenticate"/>
</form>
}
41 changes: 41 additions & 0 deletions webui/src/pages/Auth/Flow.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@import "../../styles/container";
@import "../../styles/button";
@import "../../styles/variables";

.flow {
@include container;
width: 50%;
margin-left: auto;
margin-right: auto;
min-width: 250px;

button, input[type=submit], input[type=reset] {
@include button;
}

input[type=reset] {
background-color: $grey-label;
color: $grey-text;

&:hover {
background-color: rgba($grey-label, 0.7);
}

&:active {
background-color: rgba($grey-label, 0.8);
}
}

h3 {
margin: 0;
}

ul {
list-style: none;
padding: 0;

li {
margin-bottom: 1em;
}
}
}
Loading
Loading