#168: Got successful Oauth2 implementation with Gmail OpenAPIls

This commit is contained in:
frikky
2021-10-14 02:06:24 +02:00
parent b15ac9f4d5
commit 66ed72b241
11 changed files with 406 additions and 178 deletions
+8 -6
View File
@@ -1378,7 +1378,7 @@ class AppBase:
except IndexError:
print("[DEBUG] INDEXERROR: ", parsersplit[outercnt])
#ret = innervalue
ret, tmp_loop = recurse_json(innervalue, parsersplit[outercnt:])
ret, tmp_loop = recurse_json(basejson[i], parsersplit[outercnt:])
newvalue.append(ret)
@@ -1738,7 +1738,7 @@ class AppBase:
#self.logger.debug(f"\n\nHandle static data with JSON: {data}\n\n")
#self.logger.info("STATIC PARSED: %s" % actualitem)
if len(actualitem) > 0:
self.logger.info("ACTUAL: ", actualitem)
#self.logger.info("[DEACTUAL: ", actualitem)
for replace in actualitem:
try:
to_be_replaced = replace[0]
@@ -1774,7 +1774,7 @@ class AppBase:
#self.logger.info("VALUE: %s" % parameter["value"])
if parameter["variant"] == "WORKFLOW_VARIABLE":
self.logger.info("Handling workflow variable")
self.logger.info("[DEBUG] Handling workflow variable")
found = False
try:
for item in fullexecution["workflow"]["workflow_variables"]:
@@ -2533,7 +2533,8 @@ class AppBase:
break
except TypeError as e:
newres = ""
errorstring = "%s" % e
self.logger.info(f"[DEBUG] Got exec error: {errorstring}")
errorstring = f"{e}"
if "got an unexpected keyword argument" in errorstring:
fieldsplit = errorstring.split("'")
if len(fieldsplit) > 1:
@@ -2541,7 +2542,7 @@ class AppBase:
try:
del params[field]
self.logger.info("Removed field invalid field %s" % field)
self.logger.info("[WARNING] Removed field invalid field %s" % field)
except KeyError:
break
else:
@@ -2774,7 +2775,8 @@ class AppBase:
try:
self.action_result["result"] = json.dumps({
"success": False,
"reason": f"Request error - failing silently. Details: {e}"
"reason": f"Request error - failing silently. Details in detail section",
"details": f"{e}",
})
except json.decoder.JSONDecodeError as e:
self.action_result["result"] = f"Request error: {e}"
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/docker/go-units v0.4.0 // indirect
github.com/elastic/go-elasticsearch/v7 v7.13.1 // indirect
github.com/frikky/kin-openapi v0.39.0
github.com/frikky/shuffle-shared v0.1.13
github.com/frikky/shuffle-shared v0.1.14
github.com/fsouza/go-dockerclient v1.7.2
github.com/ghodss/yaml v1.0.0
github.com/go-git/go-billy/v5 v5.0.0
+21 -6
View File
@@ -1985,8 +1985,10 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
location := strings.Split(request.URL.String(), "/")
var hookId string
var queries string
if location[1] == "api" {
if len(location) <= 4 {
log.Printf("[INFO] Couldn't handle location. Too short in webhook: %d", len(location))
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
@@ -1995,10 +1997,20 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
hookId = location[4]
}
if strings.Contains(hookId, "?") {
splitter := strings.Split(hookId, "?")
hookId = splitter[0]
if len(splitter) > 1 {
queries = splitter[1]
}
}
// ID: webhook_<UID>
if len(hookId) != 44 {
log.Printf("[INFO] Couldn't handle hookId. Too short in webhook: %d", len(hookId))
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "message": "ID not valid"}`))
resp.Write([]byte(`{"success": false, "reason": "Hook ID not valid"}`))
return
}
@@ -2022,19 +2034,19 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
if hook.Status == "stopped" {
log.Printf("[WARNING] Not running %s because hook status is stopped", hook.Id)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Click start to start it"}`)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Is it running?"}`)))
return
}
if len(hook.Workflows) == 0 {
log.Printf("Not running because hook isn't connected to any workflows")
log.Printf("[DEBUG] Not running because hook isn't connected to any workflows")
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflows are defined"}`)))
return
}
if hook.Environment == "cloud" {
log.Printf("This should trigger in the cloud. Duplicate action allowed onprem.")
log.Printf("[DEBUG] This should trigger in the cloud. Duplicate action allowed onprem.")
}
// Check auth
@@ -2050,12 +2062,16 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Body data error: %s", err)
log.Printf("[DEBUG] Body data error: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
if len(queries) > 0 && len(body) == 0 {
body = []byte(queries)
}
//log.Printf("BODY: %s", parsedBody)
// This is a specific fix for MSteams and may fix other things as well
@@ -5862,7 +5878,6 @@ func initHandlers() {
r.HandleFunc("/api/v1/triggers/gmail/register", shuffle.HandleNewGmailRegister).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/triggers/gmail/getFolders", shuffle.HandleGetGmailFolders).Methods("GET", "OPTIONS")
// FIXME: This should only be cloud. Done locally with ngrok to test
//r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/triggers/gmail/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS")
+2 -2
View File
@@ -1,7 +1,7 @@
version: '3'
services:
frontend:
#build: ./frontend
build: ./frontend
image: ghcr.io/frikky/shuffle-frontend:nightly
container_name: shuffle-frontend
hostname: shuffle-frontend
@@ -16,7 +16,7 @@ services:
depends_on:
- backend
backend:
#build: ./backend
build: ./backend
image: ghcr.io/frikky/shuffle-backend:nightly
container_name: shuffle-backend
hostname: ${BACKEND_HOSTNAME}
+1 -1
View File
@@ -78,7 +78,7 @@ const App = (message, props) => {
.then(response => response.json())
.then(responseJson => {
if (responseJson.success === true && responseJson.notifications !== null && responseJson.notifications !== undefined && responseJson.notifications.length > 0) {
console.log("RESP: ", responseJson)
//console.log("RESP: ", responseJson)
setNotifications(responseJson.notifications)
}
})
+58 -15
View File
@@ -2,9 +2,22 @@ import React, {useRef, useState, useEffect, useLayoutEffect} from 'react';
import { useTheme } from '@material-ui/core/styles';
import { v4 as uuidv4 } from 'uuid';
import { TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade } from '@material-ui/core';
import { ListItemText, TextField, Drawer, Button, Paper, Grid, Tabs, InputAdornment, Tab, ButtonBase, Tooltip, Select, MenuItem, Divider, Dialog, Modal, DialogActions, DialogTitle, InputLabel, DialogContent, FormControl, IconButton, Menu, Input, FormGroup, FormControlLabel, Typography, Checkbox, Breadcrumbs, CircularProgress, Switch, Fade } from '@material-ui/core';
import { LockOpen as LockOpenIcon } from '@material-ui/icons';
const ITEM_HEIGHT = 55
const ITEM_PADDING_TOP = 8
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
minWidth: 500,
maxWidth: 500,
scrollX: "auto",
},
},
}
const AuthenticationOauth2 = (props) => {
const { saveWorkflow, selectedApp, workflow, selectedAction, authenticationType, getAppAuthentication, appAuthentication, setSelectedAction, setNewAppAuth, setAuthenticationModalOpen} = props;
const theme = useTheme();
@@ -15,6 +28,8 @@ const AuthenticationOauth2 = (props) => {
const [clientSecret, setClientSecret] = React.useState(defaultConfigSet ? authenticationType.client_secret : "")
const [oauthUrl, setOauthUrl] = React.useState("")
const [buttonClicked, setButtonClicked] = React.useState(false)
const [selectedScopes, setSelectedScopes] = React.useState([])
const allscopes = authenticationType.scope !== undefined ? authenticationType.scope: []
const [manuallyConfigure, setManuallyConfigure] = React.useState(defaultConfigSet ? false : true)
const [authenticationOption, setAuthenticationOptions] = React.useState({
@@ -32,22 +47,16 @@ const AuthenticationOauth2 = (props) => {
return null
}
const handleOauth2Request = (client_id, client_secret, oauth_url) => {
const handleOauth2Request = (client_id, client_secret, oauth_url, scopes) => {
setButtonClicked(true)
//if (authenticationType.type === "oauth2" && authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null) {
// These are test credentials
//const client_id = "dae24316-4bec-4832-b660-4cba6dc2477b"
//const client_secret = "._Qu3EvYY-OW_D57uy79qwEo.32qD6.l0z"
const authentication_url = authenticationType.token_uri
console.log("SCOPES: ", scopes)
var resources = ""
console.log("SCOPES: ", resources)
if (authenticationType.scope !== undefined && authenticationType.scope !== null) {
resources = authenticationType.scope.join(",")
if (scopes !== undefined && scopes !== null & scopes.length > 0) {
resources = scopes.join(",")
}
resources = ["AaaServer.profile.READ"]
const authentication_url = authenticationType.token_uri
console.log("SCOPES2: ", resources)
const redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication`
@@ -68,7 +77,7 @@ const AuthenticationOauth2 = (props) => {
// How can we properly try-catch without breaks on error?
try {
var newwin = window.open(url, "", "width=400,height=200")
var newwin = window.open(url, "", "width=800,height=600")
//console.log(newwin)
var open = true
@@ -176,6 +185,18 @@ const AuthenticationOauth2 = (props) => {
}
const handleScopeChange = (event) => {
const {
target: { value },
} = event;
console.log("VALUE: ", value)
// On autofill we get a the stringified value.
setSelectedScopes(typeof value === 'string' ? value.split(',') : value)
}
if (authenticationOption.label === null || authenticationOption.label === undefined) {
authenticationOption.label = selectedApp.name+" authentication"
}
@@ -282,6 +303,29 @@ const AuthenticationOauth2 = (props) => {
</div>
)
})}
{allscopes.length === 0 ? null :
<Select
multiple
value={selectedScopes}
style={{backgroundColor: theme.palette.inputColor, color: "white", }}
onChange={(e) => {
handleScopeChange(e)
}}
fullWidth
input={<Input id="select-multiple-native" />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{allscopes.map((data, index) => {
return (
<MenuItem key={index} value={data}>
<Checkbox checked={selectedScopes.indexOf(data) > -1} />
<ListItemText primary={data} />
</MenuItem>
)
})}
</Select>
}
<TextField
style={{marginTop: 20, backgroundColor: theme.palette.inputColor, borderRadius: theme.palette.borderRadius,}}
InputProps={{
@@ -328,8 +372,7 @@ const AuthenticationOauth2 = (props) => {
variant="contained"
fullWidth
onClick={() => {
//setAuthenticationModalOpen(false)
handleOauth2Request(clientId, clientSecret, oauthUrl)
handleOauth2Request(clientId, clientSecret, oauthUrl, selectedScopes)
}}
color="primary"
>
+1 -1
View File
@@ -1616,7 +1616,7 @@ const ParsedAction = (props) => {
selectedAction.authentication_id = ""
for (var key in selectedAction.parameters) {
console.log(selectedAction.parameters[key])
//console.log(selectedAction.parameters[key])
if (selectedAction.parameters[key].configuration) {
selectedAction.parameters[key].value = ""
}
+16 -15
View File
@@ -31,8 +31,8 @@ import { GetParsedPaths } from "./Apps.jsx";
import ConfigureWorkflow from '../components/ConfigureWorkflow.jsx';
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
import ParsedAction from '../components/ParsedAction.jsx';
import Scroll from 'react-scroll'
import { Element as ScrollElement, animateScroll as scroll, scrollSpy, scroller } from 'react-scroll'
//import Scroll from 'react-scroll'
//import { Element as ScrollElement, animateScroll as scroll, scrollSpy, scroller } from 'react-scroll'
const surfaceColor = "#27292D"
const inputColor = "#383B40"
@@ -1008,6 +1008,7 @@ const AngularWorkflow = (props) => {
if (hasSaved === false) {
//alert.error("You might have forgotten to save before executing.")
//const saveWorkflow = (curworkflow) => {
//setExecutionRunning(true)
setExecutionRequestStarted(true)
saveWorkflow(workflow, executionArgument, startNode)
console.log("FIXME: Might have forgotten to save before executing.")
@@ -3013,10 +3014,10 @@ const AngularWorkflow = (props) => {
}
break;
case 83:
if (previouskey === 17) {
event.preventDefault()
saveWorkflow()
}
//if (previouskey === 17) {
// event.preventDefault()
// saveWorkflow()
//}
break;
case 70:
//if (previouskey === 17) {
@@ -3158,7 +3159,7 @@ const AngularWorkflow = (props) => {
setEstablished(true)
// Validate if the node is just a node lol
console.log("CY: ", cy)
//console.log("CY: ", cy)
//console.log("CY: ", cy.edgehandles())
//try {
cy.edgehandles({
@@ -5796,7 +5797,7 @@ const AngularWorkflow = (props) => {
const url = `https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent&client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${scopes}&state=workflow_id%3D${props.match.params.key}%26trigger_id%3D${selectedTrigger.id}%26username%3D${username}%26type%3Dgmail%26start%3d${startnode}`
console.log("URL: ", url)
var newwin = window.open(url, "", "width=200,height=100")
var newwin = window.open(url, "", "width=800,height=600")
// Check whether we got a callback somewhere
var id = setInterval(function () {
@@ -5871,7 +5872,7 @@ const AngularWorkflow = (props) => {
console.log("URL: ", url)
var newwin = window.open(url, "", "width=200,height=100")
var newwin = window.open(url, "", "width=800,height=600")
// Check whether we got a callback somewhere
var id = setInterval(function () {
@@ -9406,10 +9407,10 @@ const AngularWorkflow = (props) => {
//console.log("SCROLL IS NOT 0: ", scrollConfig.top)
//rightSideActionView.scrollTop = scrollConfig.top
setTimeout(() => {
scroller.scrollTo('elements_wrapper', {
containerId: 'rightside_actions',
offset: scrollConfig.top,
})
//scroller.scrollTo('elements_wrapper', {
// containerId: 'rightside_actions',
// offset: scrollConfig.top,
//})
if (scrollConfig.selected !== undefined && scrollConfig.selected !== null) {
const selectedField = document.getElementById(scrollConfig.selected)
@@ -9437,15 +9438,15 @@ const AngularWorkflow = (props) => {
return (
<div>
<ScrollElement name="elements_wrapper">
<Prompt
when={!lastSaved}
message={unloadText}
/>
{loadedCheck}
</ScrollElement>
</div>
)
// <ScrollElement name="elements_wrapper">
// </ScrollElement>
}
export default AngularWorkflow
+162 -53
View File
@@ -222,7 +222,7 @@ const AppCreator = (props) => {
const [parameterName, setParameterName] = useState("");
const [parameterLocation, setParameterLocation] = useState(apikeySelection.length > 0 ? apikeySelection[0] : "");
const [refreshUrl, setRefreshUrl] = useState("");
const [oauth2Scopes, setOauth2Scopes] = useState(["OAUTH2.SCOPE.HERE"]);
const [oauth2Scopes, setOauth2Scopes] = useState([]);
const [projectCategories, setProjectCategories] = useState([]);
const [selectedCategory, setSelectedCategory] = useState("");
@@ -396,7 +396,7 @@ const AppCreator = (props) => {
const handleGetRef = (parameter, data) => {
try {
if (parameter === null || parameter["$ref"] === undefined) {
//console.log("$ref not found in getref: ")
//console.log("$ref not found in getref for: ", parameter)
return parameter
}
} catch (e) {
@@ -425,12 +425,6 @@ const AppCreator = (props) => {
}
return newitem
//console.log("Should get ", parameter["$ref"])
//const subkeys = parameter["$ref"].split("/")
// setBasedata(data)
// handleGetReference(parameter["$ref"])
}
// Sets the data up as it should be at later points
@@ -476,8 +470,19 @@ const AppCreator = (props) => {
document.title = "Apps - "+data.info.title
if (data.info["x-logo"] !== undefined) {
if (data.info["x-logo"].url !== undefined) {
console.log("PARSED LOGO: ", data.info["x-logo"].url)
setFileBase64(data.info["x-logo"].url)
} else {
setFileBase64(data.info["x-logo"])
}
console.log("")
console.log("")
console.log("LOGO: ", data.info["x-logo"])
console.log("")
console.log("")
}
if (data.info.contact !== undefined) {
setContact(data.info.contact)
@@ -542,8 +547,11 @@ const AppCreator = (props) => {
}
if (!allowedfunctions.includes(method.toUpperCase())) {
// Typical YAML issue
if (method !== "parameters") {
console.log("Invalid method: ", method, "data: ", methodvalue)
alert.info("Skipped method (not allowed): "+method)
}
continue
}
@@ -552,6 +560,8 @@ const AppCreator = (props) => {
tmpname = methodvalue.operationId
}
tmpname = tmpname.replaceAll(".", " ")
var newaction = {
"name": tmpname,
"description": methodvalue.description,
@@ -582,10 +592,8 @@ const AppCreator = (props) => {
}
}
//console.log("Category:
//console.log("Schema is application/json: ", methodvalue)
//console.log("DATA", data)
// Typescript? I think not ;)
if (methodvalue["requestBody"] !== undefined) {
//console.log("Handle requestbody: ", methodvalue["requestBody"])
if (methodvalue["requestBody"]["content"] !== undefined) {
@@ -637,15 +645,13 @@ const AppCreator = (props) => {
}
}
} else {
//console.log("REQUESTBODY: ", methodvalue["requestBody"]["content"])
if (methodvalue["requestBody"]["content"]["example"] !== undefined) {
if (methodvalue["requestBody"]["content"]["example"]["example"] !== undefined) {
newaction["body"] = methodvalue["requestBody"]["content"]["example"]["example"]
//JSON.stringify(tmpobject, null, 2)
}
}
//console.log(methodvalue["requestBody"]["content"])
if (methodvalue["requestBody"]["content"]["multipart/form-data"] !== undefined) {
} else if (methodvalue["requestBody"]["content"]["multipart/form-data"] !== undefined) {
if (methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"] !== undefined && methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"] !== null) {
if (methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["type"] === "object") {
const fieldname = methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["properties"]["fieldname"]
@@ -655,6 +661,63 @@ const AppCreator = (props) => {
}
}
}
} else {
var schemas = []
const content = methodvalue["requestBody"]["content"]
if (content !== undefined && content !== null) {
//console.log("CONTENT: ", content)
for (const [subkey, subvalue] of Object.entries(content)) {
if (subvalue["schema"] !== undefined) {
//console.log("SCHEMA: ", subvalue["schema"])
if (subvalue["schema"]["$ref"] !== undefined) {
//console.log("SCHEMA FOUND REF!")
if (!schemas.includes(subvalue["schema"]["$ref"])) {
schemas.push(subvalue["schema"]["$ref"])
}
}
} else {
console.log("ERROR: couldn't find schema for ", subvalue, method)
}
}
}
if (schemas.length === 1) {
const parameter = handleGetRef({"$ref": schemas[0]}, data)
if (parameter.properties !== undefined && parameter["type"] === "object") {
var newbody = {}
for (var propkey in parameter.properties) {
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
if (parameter.properties[propkey].type === undefined) {
console.log("Skipping (4): ", parameter.properties[propkey])
continue
}
if (parameter.properties[propkey].type === "string") {
if (parameter.properties[propkey].description !== undefined) {
newbody[parsedkey] = parameter.properties[propkey].description
} else {
newbody[parsedkey] = ""
}
} else if (parameter.properties[propkey].type.includes("int") || parameter.properties[propkey].type.includes("uint64")) {
newbody[parsedkey] = 0
} else if (parameter.properties[propkey].type.includes("boolean")) {
newbody[parsedkey] = false
} else if (parameter.properties[propkey].type.includes("array")) {
newbody[parsedkey] = []
} else {
console.log("CANT HANDLE JSON TYPE (4)", parameter.properties[propkey].type, parameter.properties[propkey])
newbody[parsedkey] = []
}
}
newaction["body"] = JSON.stringify(newbody, null, 2)
} else {
console.log("CANT HANDLE PARAM: (4) ", parameter.properties)
}
}
}
}
}
@@ -696,7 +759,7 @@ const AppCreator = (props) => {
for (var propkey in parameter.properties) {
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
if (parameter.properties[propkey].type === undefined) {
console.log("Skipping: ", parameter.properties[propkey])
console.log("Skipping (1): ", parameter.properties[propkey])
continue
}
@@ -708,6 +771,13 @@ const AppCreator = (props) => {
}
} else if (parameter.properties[propkey].type.includes("int")) {
newbody[parsedkey] = 0
} else if (parameter.properties[propkey].type.includes("boolean")) {
newbody[parsedkey] = false
} else if (parameter.properties[propkey].type.includes("array")) {
//console.log("Added empty array. Base is: ", parameter.properties[propkey].type)
//const parameter = handleGetRef(selectedExample["content"]["application/json"]["schema"], data)
newbody[parsedkey] = []
} else {
console.log("CANT HANDLE JSON TYPE ", parameter.properties[propkey].type, parameter.properties[propkey])
newbody[parsedkey] = []
@@ -732,7 +802,7 @@ const AppCreator = (props) => {
for (var propkey in parameter.properties) {
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
if (parameter.properties[propkey].type === undefined) {
console.log("Skipping: ", parameter.properties[propkey])
console.log("Skipping (2): ", parameter.properties[propkey])
continue
}
@@ -744,6 +814,8 @@ const AppCreator = (props) => {
}
} else if (parameter.properties[propkey].type.includes("int")) {
newbody[parsedkey] = 0
} else if (parameter.properties[propkey].type.includes("boolean")) {
newbody[parsedkey] = false
} else {
console.log("CANT HANDLE JSON TYPE (2) ", parameter.properties[propkey].type)
newbody[parsedkey] = []
@@ -767,7 +839,7 @@ const AppCreator = (props) => {
for (var propkey in parameter.properties) {
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
if (parameter.properties[propkey].type === undefined) {
console.log("Skipping: ", parameter.properties[propkey])
console.log("Skipping (3): ", parameter.properties[propkey])
continue
}
@@ -790,7 +862,7 @@ const AppCreator = (props) => {
//newaction.example_response = JSON.stringify(parameter.properties, null, 2)
} else {
//newaction.example_response = parameter.properties
console.log("CANT HANDLE PARAM: (2) ", parameter.properties)
console.log("CANT HANDLE PARAM: (3) ", parameter.properties)
}
}
}
@@ -894,6 +966,7 @@ const AppCreator = (props) => {
newaction.errors.push("Missing name")
}
}
newActions.push(newaction)
}
}
@@ -924,33 +997,71 @@ const AppCreator = (props) => {
}
console.log("SECURITYSCHEMES: ", securitySchemes)
//console.log("SECURITYSCHEMES: ", securitySchemes)
if (securitySchemes !== undefined) {
// FIXME: Should add Oauth2 (Microsoft) and JWT (Wazuh)
//console.log("SECURITY: ", securitySchemes)
//if (Object.entries(securitySchemes) > 1 &&
var newauth = []
for (const [key, value] of Object.entries(securitySchemes)) {
console.log(key, value)
//console.log(key, value)
if (value.scheme === "bearer") {
setAuthenticationOption("Bearer auth")
setAuthenticationRequired(true)
} else if (key === "Oauth2") {
} else if (key === "Oauth2" || key === "Oauth2c") {
//alert.info("Can't handle Oauth2 auth yet.")
setAuthenticationOption("Oauth2")
setAuthenticationRequired(true)
if (value.flow.authorizationCode.authorizationUrl !== undefined) {
setParameterName(value.flow.authorizationCode.authorizationUrl)
//console.log("FLOW-1: ", value)
const flowkey = value.flow === undefined ? "flows" : "flow"
//console.log("FLOW: ", value[flowkey])
const basekey = value[flowkey].authorizationCode !== undefined ? "authorizationCode" : "implicit"
//console.log("FLOW2: ", value[flowkey][basekey])
if (value[flowkey] !== undefined && value[flowkey][basekey] !== undefined) {
if (value[flowkey][basekey].authorizationUrl !== undefined && parameterName.length === 0) {
setParameterName(value[flowkey][basekey].authorizationUrl)
}
if (value.flow.authorizationCode.tokenUrl !== undefined) {
setParameterLocation(value.flow.authorizationCode.tokenUrl)
var tokenUrl = ""
if (value[flowkey][basekey].tokenUrl !== undefined) {
setParameterLocation(value[flowkey][basekey].tokenUrl)
tokenUrl = value[flowkey][basekey].tokenUrl
} else {
setParameterLocation("")
}
if (value.flow.authorizationCode.refreshUrl !== undefined) {
setRefreshUrl(value.flow.authorizationCode.refreshUrl)
if (value[flowkey][basekey].refreshUrl !== undefined) {
setRefreshUrl(value[flowkey][basekey].refreshUrl)
} else if (tokenUrl.length > 0) {
setRefreshUrl(tokenUrl)
}
if (value.flow.authorizationCode.scopes !== undefined && value.flow.authorizationCode.scopes !== null && value.flow.authorizationCode.scopes.length > 0) {
setOauth2Scopes(value.flow.authorizationCode.scopes)
if (value[flowkey][basekey].scopes !== undefined && value[flowkey][basekey].scopes !== null) {
if (value[flowkey][basekey].scopes.length > 0) {
setOauth2Scopes(value[flowkey][basekey].scopes)
} else {
var newscopes = []
for (let [scopekey, scopevalue] of Object.entries(value[flowkey][basekey].scopes)) {
if (scopekey.startsWith("http")) {
const scopekeysplit = scopekey.split("/")
if (scopekeysplit.length < 5) {
console.log("Skipping scope: ", scopekey)
alert.info("Skipping scope: "+scopekey)
continue
}
//console.log("Checking scope for: ", scopekey, scopekeysplit.length)
}
newscopes.push(scopekey)
}
setOauth2Scopes(newscopes)
}
}
} else {
console.log("Bad flowkey and basekey for oauth2: ", flowkey, basekey)
}
} else if (key === "ApiKeyAuth") {
@@ -973,11 +1084,12 @@ const AppCreator = (props) => {
setAuthenticationOption("Oauth2")
setAuthenticationRequired(true)
} else {
newauth.push({
"name": key,
"type": value.in,
"example": "",
})
alert.error("Couldn't handle AUTH type: ", key)
//newauth.push({
// "name": key,
// "type": value.in,
// "example": "",
//})
}
}
@@ -1446,15 +1558,6 @@ const AppCreator = (props) => {
},
},
}
console.log("SCOPES: ", oauth2Scopes)
//if (oauth2Scopes.scopes > 0) {
// for (var key in oauth2Scopes) {
// const scope = oauth2Scopes[key]
// data.components.securitySchemes["Oauth2"]["flow"]["authorizationCode"]["scopes"].push(scope)
// }
//}
}
if (setExtraAuth.length > 0) {
@@ -1826,21 +1929,25 @@ const AppCreator = (props) => {
InputProps={{
style:{
color: "white",
maxHeight: 50,
},
}}
style={{maxHeight: 80, overflowX: "hidden", overflowY: "auto",}}
placeholder="Scopes"
color="primary"
fullWidth
defaultValue={oauth2Scopes}
value={oauth2Scopes}
onAdd={(chip) => {
oauth2Scopes.push(chip)
console.log(oauth2Scopes)
setOauth2Scopes(oauth2Scopes)
setUpdate(Math.random())
}}
onDelete={(chip, index) => {
oauth2Scopes.splice(index, 1)
console.log(oauth2Scopes)
setOauth2Scopes(oauth2Scopes)
setUpdate(Math.random())
}}
/>
</div>
@@ -3265,11 +3372,15 @@ const AppCreator = (props) => {
0, 0, canvas.width, canvas.height
)
try {
const canvasUrl = canvas.toDataURL()
if (canvasUrl !== fileBase64) {
//console.log("SET URL TO: ", canvasUrl)
setFileBase64(canvasUrl)
}
} catch (e) {
alert.error("Failed to parse canvasurl!")
}
}
//console.log(img.width)
@@ -3278,12 +3389,6 @@ const AppCreator = (props) => {
//canvas.height = img.height
}
//const imageInfo = file.length === 0 ?
// <div style={{textAlign: "center", marginTop: 20}}>
// Upload logo
// </div> :
// <img src={file} id="logo" style={{width: "100%", height: "100%"}} />
const [imageUploadError, setImageUploadError] = useState("");
const [openImageModal, setOpenImageModal] = useState("");
const [scale, setScale] = useState(1);
@@ -3293,7 +3398,7 @@ const AppCreator = (props) => {
let imageData = fileBase64;
let croppedData = file.length > 0 ? file : fileBase64
const imageInfo = <img src={imageData} id="logo" style={{maxWidth: 174, maxHeight: 174, minWidth: 174, minHeight: 174, objectFit: "contain",}} />
const imageInfo = <img crossorigin="anonymous" src={imageData} id="logo" style={{maxWidth: 174, maxHeight: 174, minWidth: 174, minHeight: 174, objectFit: "contain",}} />
const alternateImg = <AddPhotoAlternateIcon style={{ width: 100, height: 100, flex: "1", display: "flex", flexDirection: "row", margin: "auto", marginTop: 30, marginLeft: 40,}} onClick={() => {
upload.click()
@@ -3326,11 +3431,15 @@ const AppCreator = (props) => {
const onSaveAppIcon = () => {
if(editor){
try {
setFile("");
const canvas = editor.getImageScaledToCanvas();
setFileBase64(canvas.toDataURL());
setFileBase64(canvas.toDataURL())
setOpenImageModal(false)
setDisableImageUpload(true);
} catch (e) {
alert.error("Failed to set image. Replace it if this persists.")
}
}
}
+3 -3
View File
@@ -845,7 +845,7 @@ const Apps = (props) => {
<h2>App Creator</h2>
<a href="https://shuffler.io/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">How it works</a>
&nbsp;- <a href="https://github.com/frikky/security-openapis" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">Security API's</a>
&nbsp;- <a href="https://apis.guru/browse-apis/" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">OpenAPI directory</a>
&nbsp;- <a href="https://github.com/APIs-guru/openapi-directory/tree/main/APIs" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">OpenAPI directory</a>
&nbsp;- <a href="https://editor.swagger.io/" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">OpenAPI Validator</a>
<div/>
<Typography variant="body2" color="textSecondary">
@@ -1073,10 +1073,10 @@ const Apps = (props) => {
<CircularProgress style={{width: 40, height: 40, margin: "auto"}}/>
:
<Paper square style={uploadViewPaperStyle}>
<Typography variant="h6" style={{margin: 10}}>
<Typography variant="body1" style={{margin: 10}}>
No apps have been created, uploaded or downloaded yet. Click "Load existing apps" above to get the baseline. This may take a while as its building docker images.
</Typography>
<Typography variant="h6" style={{margin: 10}}>
<Typography variant="body1" style={{margin: 10}}>
If you're still not able to see any apps, please follow our <a href={"https://shuffler.io/docs/troubleshooting#load_all_apps_locally"} style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">troubleshooting guide for loading apps!</a>
</Typography>
</Paper>
+119 -61
View File
@@ -5,6 +5,7 @@ import { useTheme } from '@material-ui/core/styles';
import {Avatar, Grid, Paper, Tooltip, Divider, Button, TextField, FormControl, IconButton, Menu, MenuItem, FormControlLabel, Chip, Switch, Typography, Zoom, CircularProgress, Dialog, DialogTitle, DialogActions, DialogContent} from '@material-ui/core';
import {GridOn as GridOnIcon, List as ListIcon, Close as CloseIcon, Compare as CompareIcon, Maximize as MaximizeIcon, Minimize as MinimizeIcon, AddCircle as AddCircleIcon, Toc as TocIcon, Send as SendIcon, Search as SearchIcon, FileCopy as FileCopyIcon, Delete as DeleteIcon, BubbleChart as BubbleChartIcon, Restore as RestoreIcon, Cached as CachedIcon, GetApp as GetAppIcon, Apps as AppsIcon, Edit as EditIcon, MoreVert as MoreVertIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Publish as PublishIcon, CloudUpload as CloudUploadIcon, CloudDownload as CloudDownloadIcon} from '@material-ui/icons';
import NestedMenuItem from "material-ui-nested-menu-item";
//import {Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons';
//https://next.material-ui.com/components/material-icons/
@@ -27,6 +28,7 @@ import CytoscapeWrapper from '../components/RenderCytoscape'
const inputColor = "#383B40"
const surfaceColor = "#27292D"
const svgSize = 24
const imagesize = 22
const flexContainerStyle = {
display: "flex",
@@ -1102,7 +1104,6 @@ const Workflows = (props) => {
)
}
const WorkflowPaper = (props) => {
const { data } = props;
const [open, setOpen] = React.useState(false);
@@ -1135,7 +1136,69 @@ const Workflows = (props) => {
const actions = data.actions !== null ? data.actions.length : 0
const [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data)
const imagesize = 22
const workflowMenuButtons = <Menu
id="long-menu"
anchorEl={anchorEl}
keepMounted
open={open}
onClose={() => {
setOpen(false)
setAnchorEl(null)
}}
>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
//console.log("DATA:" ,data)
setModalOpen(true)
setEditingWorkflow(data)
setNewWorkflowName(data.name)
setNewWorkflowDescription(data.description)
setDefaultReturnValue(data.default_return_value)
if (data.tags !== undefined && data.tags !== null) {
setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags)))
}
}} key={"change"}>
<EditIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Change details"}
</MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
setSelectedWorkflow(data)
setPublishModalOpen(true)
}} key={"publish"}>
<CloudUploadIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Publish Workflow"}
</MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
copyWorkflow(data)
setOpen(false)
}} key={"duplicate"}>
<FileCopyIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Duplicate Workflow"}
</MenuItem>
<NestedMenuItem disabled={userdata.orgs === undefined || userdata.orgs === null || userdata.orgs.length === 1 || userdata.orgs.length >= 0} style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
//copyWorkflow(data)
//setOpen(false)
}} key={"duplicate"}>
<FileCopyIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Copy to Child Org"}
</NestedMenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
setExportModalOpen(true)
setExportData(data)
setOpen(false)
}} key={"export"}>
<GetAppIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Export Workflow"}
</MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
setDeleteModalOpen(true)
setSelectedWorkflowId(data.id)
setOpen(false)
}} key={"delete"}>
<DeleteIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Delete Workflow"}
</MenuItem>
</Menu>
var image = ""
var orgName = ""
@@ -1283,62 +1346,7 @@ const Workflows = (props) => {
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
anchorEl={anchorEl}
keepMounted
open={open}
onClose={() => {
setOpen(false)
setAnchorEl(null)
}}
>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
//console.log("DATA:" ,data)
setModalOpen(true)
setEditingWorkflow(data)
setNewWorkflowName(data.name)
setNewWorkflowDescription(data.description)
setDefaultReturnValue(data.default_return_value)
if (data.tags !== undefined && data.tags !== null) {
setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags)))
}
}} key={"change"}>
<EditIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Change details"}
</MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
setSelectedWorkflow(data)
setPublishModalOpen(true)
}} key={"publish"}>
<CloudUploadIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Publish Workflow"}
</MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
copyWorkflow(data)
setOpen(false)
}} key={"duplicate"}>
<FileCopyIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Duplicate Workflow"}
</MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
setExportModalOpen(true)
setExportData(data)
setOpen(false)
}} key={"export"}>
<GetAppIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Export Workflow"}
</MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
setDeleteModalOpen(true)
setSelectedWorkflowId(data.id)
setOpen(false)
}} key={"delete"}>
<DeleteIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Delete Workflow"}
</MenuItem>
</Menu>
{workflowMenuButtons}
</Grid>
{/*
<Grid>
@@ -1547,6 +1555,50 @@ const Workflows = (props) => {
let workflowData = "";
if (workflows.length > 0) {
const columns = [
{ field: 'image', headerName: 'Logo', width: 42, renderCell: (params) => {
const data = params.row.record
var boxColor = "#FECC00"
if (data.is_valid) {
boxColor = "#86c142"
}
if (!data.previously_saved) {
boxColor = "#f85a3e"
}
var image = ""
var orgName = ""
var orgId = ""
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find(org => org.id === data["org_id"])
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {width: imagesize+7, height: imagesize+7, pointerEvents: "none", marginLeft: data.creator_org !== undefined && data.creator_org.length > 0 ? 20 : 0, borderRadius: 10, border: foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null, cursor: "pointer", marginTop: 5, }
//<Tooltip title={`Org: ${foundOrg.name}`} placement="bottom">
image = foundOrg.image === "" ?
<img alt={foundOrg.name} src={theme.palette.defaultImage} style={imageStyle} />
:
<img alt={foundOrg.name} src={foundOrg.image} style={imageStyle} onClick={() => {
//setFilteredWorkflows(newWorkflows)
}}/>
orgName = foundOrg.name
orgId = foundOrg.id
}
}
return (
<div styl={{cursor: "pointer"}} onClick={() => {
//addFilter(orgId)
//setFilters(["Org "+orgName])
//setFilteredWorkflows(newWorkflows)
}}>
{image}
</div>
)
}},
{ field: 'title', headerName: 'Title', width: 330, renderCell: (params) => {
const data = params.row.record
@@ -1569,7 +1621,7 @@ const Workflows = (props) => {
</Typography>
)
} */},
{ field: 'actions', headerName: 'Options', width: 200, sortable: false,
{ field: 'options', headerName: 'Options', width: 200, sortable: false,
disableClickEventBubbling: true,
renderCell: (params) => {
const data = params.row.record;
@@ -1635,7 +1687,7 @@ const Workflows = (props) => {
)
}
},
{/* field: 'tags', headerName: 'Tags', maxHeight: 15, width: 390, sortable: false,
{ field: 'tags', headerName: 'Tags', maxHeight: 15, width: 300, sortable: false,
disableClickEventBubbling: true,
renderCell: (params) => {
const data = params.row.record;
@@ -1661,7 +1713,12 @@ const Workflows = (props) => {
</Grid>
)
}
*/}
},
{ field: '', headerName: '', maxHeight: 15, width: 100, sortable: false,
disableClickEventBubbling: true,
renderCell: (params) => {
}
}
];
let rows = [];
rows = workflows.map((data, index) => {
@@ -1732,6 +1789,7 @@ const Workflows = (props) => {
placeholder="Name"
margin="dense"
defaultValue={newWorkflowName}
autoFocus
fullWidth
/>
<TextField