Merge pull request #38 from frikky/dev

Added execution view in workflow and fixed a bunch of small bugs
This commit is contained in:
Frikky
2020-05-27 03:13:04 +09:00
committed by GitHub
69 changed files with 1458 additions and 1254 deletions
+3
View File
@@ -12,6 +12,9 @@ backend/go-app/generated*
functions/generated_apps
*.zip
*openapi-parsers/generated/*
*openapi-parsers/other/*
backend/onprem/app_sdk/apps
*test.py
+3 -2
View File
@@ -36,15 +36,16 @@ Documentation can be found on https://shuffler.io/docs/about or in your own inst
Open an issue on Github, or [join the gitter chat](https://gitter.im/Shuffle-SOAR/community). For other / private requests: [frikky@shuffler.io](mailto:frikky@shuffler.io)
### Setup - Local development
Frontend - requires [npm](https://nodejs.org/en/download/)/[yarn](https://yarnpkg.com/lang/en/docs/install/#debian-stable)/your preferred manager. Runs independently from backend - edit frontend/src/App.yaml (line 46~) from window.location.origin to http://YOUR IP:5001
Frontend - requires [npm](https://nodejs.org/en/download/)/[yarn](https://yarnpkg.com/lang/en/docs/install/#debian-stable)/your preferred manager. Runs independently from backend - edit frontend/src/App.yaml (line 44~) from window.location.origin to http://YOUR IP:5001
```bash
cd frontend
npm i
npm start
```
Backend - API calls - requires [>=go1.13](https://golang.org/dl/) and [gcloud](https://cloud.google.com/sdk/install)
Backend - API calls - requires [>=go1.13](https://golang.org/dl/)
```bash
export DATASTORE_EMULATOR_HOST=0.0.0.0:8000
cd backend/go-app
go build
go run *.go
+12 -2
View File
@@ -2,6 +2,7 @@ from golang as builder
# Add files
RUN mkdir /app
RUN mkdir /app_sdk
WORKDIR /app
ADD ./go-app/main.go /app
ADD ./go-app/walkoff.go /app
@@ -10,17 +11,26 @@ ADD ./go-app/codegen.go /app
ADD ./go-app/go.mod /app
# Required files for code generation
ADD ./app_sdk/app_base.py /app_sdk
ADD ./app_sdk/static_baseline.py /app_sdk
ADD ./app_gen /app_gen
RUN go get -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webapp .
# Certificate build
# Certificate build - gets required certs
FROM alpine:latest as certs
RUN apk --update add ca-certificates
from scratch
COPY --from=builder /app/ /
COPY --from=builder /app/ /app
COPY --from=builder /app_sdk/ /app_sdk
COPY --from=builder /app_gen/ /app_gen
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
WORKDIR /app
EXPOSE 5001
CMD ["./webapp"]
+13 -56
View File
@@ -1,61 +1,18 @@
# Backend setup
1 Go to https://console.cloud.google.com/apis/credentials?project=shuffle-241517&folder&organizationId and get credentials
2. Move the file to current folder (or make step 3 be your download folder or w/e)
3. export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/Shuffle-2a19ff64af66.json
# Backend
This folder has all parts necessary for the backend to run locally and in Docker
# Backend run testserver (appengine)
1. Set up gcloud locally
```bash
dev_appserver.py go-app/ --port=5001 --host=0.0.0.0 --enable_host_checking=false
```
## Structure
* go-app: The backend. Modify these to edit the backend API.
* database: The datastore database.
* app_sdk: The app_sdk for apps. MIT licensed.
* app_gen: Code used when generating docker images. MIT licensed
* tests: A bunch of cronscripts. There are no real, good tests yet
# Backend deploy
* I created a simple script that moves the data into your GOPATH and deploys for you. This will require more tests in the future.
## Development
Shuffle's backend is written in Go, with apps being python (for now). More about local development can be seen in the main README.
# OpenAPI spec checks
Paths:
* /path/{variablename}?queryvar= <-- variable
* ^variablename needs to be part of parameters too.
* ^queryvar needs to be part of parameters too.
Running the backend:
```
parameters:
- name: variablename
in: path
description: Blah blah
required: true/false
schema:
type: string
enum: [a, b, c] # <-- not necessary, but could be great
- name: queryvar
in: query
description: blah blah
required: true/false
schema:
type: string
enum: [a, b, c]
```
* requestBody? Not in GET, DELETE & HEAD. Can consume JSON, XML, form data, plai ntext & others. Can use markdown for the description.
* Do I care about the response? Maybe :o
```
requestBody:
description: Optional kind of description
required: false/true
content:
application/json:
schema:
type: object
additionalProperties: true
properties:
name:
type: string
fav_number:
type: integer
required:
- name
- email
encoding:
color:
style: form
explode: false
cd go-app
go run *.go
```
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Frikkylikeme
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+16
View File
@@ -0,0 +1,16 @@
FROM python:3.7-alpine as base
FROM base as builder
RUN apk --no-cache add --update alpine-sdk libffi libffi-dev musl-dev openssl-dev
RUN mkdir /install
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install --prefix="/install" -r /requirements.txt
FROM base
COPY --from=builder /install /usr/local
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Frikkylikeme
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+16
View File
@@ -0,0 +1,16 @@
# app_sdk.py
This is the SDK used for apps to behave like they should.
To change it in the backend, upload it to Buckets/shuffler.appspot.com/generated_apps/baseline.
# static_baseline.py
It's used for python code generation and should be under MIT. Has to be located here because it's used by the backend.
## If you want to update apps.. PS: downloads from docker hub do overrides.. :)
1. Write your code & check if runtime works
2. Build app_base image
3. docker rm $(docker ps -aq) # Remove all stopped containers
4. Delete the specific app's Docker image (docker rmi frikky/shuffle:...)
5. Rebuild the Docker image (click load in GUI?)
# LICENSE
Everything in here is MIT, not AGPLv3 as indicated by the license.
View File
+597
View File
@@ -0,0 +1,597 @@
import os
import sys
import re
import time
import json
import logging
import requests
class AppBase:
""" The base class for Python-based apps in Shuffle, handles logging and callbacks configurations"""
__version__ = None
app_name = None
def __init__(self, redis=None, logger=None, console_logger=None):#, docker_client=None):
self.logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
self.redis=redis
self.console_logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
# apikey is for the user / org
# authorization is for the specific workflow
self.url = os.getenv("CALLBACK_URL", "https://shuffler.io")
self.action = os.getenv("ACTION", "")
self.apikey = os.getenv("FUNCTION_APIKEY", "")
self.authorization = os.getenv("AUTHORIZATION", "")
self.current_execution_id = os.getenv("EXECUTIONID", "")
if len(self.action) == 0:
print("ACTION env not defined")
sys.exit(0)
if len(self.apikey) == 0:
print("FUNCTION_APIKEY env not defined")
sys.exit(0)
if len(self.authorization) == 0:
print("AUTHORIZATION env not defined")
sys.exit(0)
if len(self.current_execution_id) == 0:
print("EXECUTIONID env not defined")
sys.exit(0)
if isinstance(self.action, str):
self.action = json.loads(self.action)
async def execute_action(self, action):
# FIXME - add request for the function STARTING here. Use "results stream" or something
# PAUSED, AWAITING_DATA, PENDING, COMPLETED, ABORTED, EXECUTING, SUCCESS, FAILURE
# !!! Let this line stay - its used for some horrible codegeneration / stitching !!! #
#STARTCOPY
stream_path = "/api/v1/streams"
action_result = {
"action": action,
"authorization": self.authorization,
"execution_id": self.current_execution_id,
"result": "",
"started_at": int(time.time()),
"status": "EXECUTING"
}
self.logger.info("ACTION RESULT: %s", action_result)
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer %s" % self.apikey
}
# Add async logger
# self.console_logger.handlers[0].stream.set_execution_id()
#self.logger.info("Before initial stream result")
try:
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Workflow: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
except requests.exceptions.ConnectionError as e:
print("Connectionerror: %s" % e)
return
#self.logger.info("AFTER initial stream result")
self.logger.info("THIS IS THE NEW UPDATE")
# Verify whether there are any parameters with ACTION_RESULT required
# If found, we get the full results list from backend
fullexecution = {}
try:
tmpdata = {
"authorization": self.authorization,
"execution_id": self.current_execution_id
}
self.logger.info("Before FULLEXEC stream result")
ret = requests.post(
"%s/api/v1/streams/results" % (self.url),
headers=headers,
json=tmpdata
)
if ret.status_code == 200:
fullexecution = ret.json()
else:
self.logger.info("Error: Data: ", ret.json())
self.logger.info("Error with status code for results. Crashing because ACTION_RESULTS or WORKFLOW_VARIABLE can't be handled. Status: %d" % ret.status_code)
return
except requests.exceptions.ConnectionError as e:
self.logger.info("Connectionerror: %s" % e)
return
self.logger.info("AFTER FULLEXEC stream result")
# Takes a workflow execution as argument
# Returns a string if the result is single, or a list if it's a list
def get_json_value(execution_data, input_data):
parsersplit = input_data.split(".")
actionname = parsersplit[0][1:].replace(" ", "_", -1)
print(f"Actionname: {actionname}")
# 1. Find the action
baseresult = ""
try:
if actionname.lower() == "exec":
baseresult = execution_data["execution_argument"]
else:
for result in execution_data["results"]:
resultlabel = result["action"]["label"].replace(" ", "_", -1).lower()
if resultlabel.lower() == actionname.lower():
baseresult = result["result"]
break
except KeyError as error:
print(f"Error: {error}")
print(f"After first trycatch")
# 2. Find the JSON data
if len(baseresult) == 0:
return ""
if len(parsersplit) == 1:
return baseresult
baseresult = baseresult.replace("\'", "\"")
basejson = {}
try:
basejson = json.loads(baseresult)
except json.decoder.JSONDecodeError as e:
return baseresult
try:
cnt = 0
for value in parsersplit[1:]:
cnt += 1
if value == "#":
# FIXME - not recursive - should go deeper if there are more #
print("HANDLE RECURSIVE LOOP ")
returnlist = []
for innervalue in basejson:
#print("Value: %s" % value[parsersplit[cnt+1]])
returnlist.append(innervalue[parsersplit[cnt+1]])
# Example format: ${[]}$
return "${%s%s}$" % (parsersplit[cnt+1], json.dumps(returnlist))
else:
if isinstance(basejson[value], str):
print(f"LOADING STRING '%s' AS JSON" % basejson[value])
try:
basejson = json.loads(basejson[value])
except json.decoder.JSONDecodeError as e:
print("RETURNING BECAUSE '%s' IS A NORMAL STRING" % basejson[value])
return basejson[value]
else:
basejson = basejson[value]
except KeyError as e:
return "KeyError: %s" % e
except IndexError as e:
return "IndexError: %s" % e
return basejson
def parse_params(action, fullexecution, parameter):
jsonparsevalue = "$."
match = ".*([$]{1}([a-zA-Z0-9()# _-]+\.?){1,})"
# Regex to find all the things
if parameter["variant"] == "STATIC_VALUE":
data = parameter["value"]
self.logger.debug(f"\n\nHandle static data with JSON: {data}\n\n")
actualitem = re.findall(match, data, re.MULTILINE)
self.logger.info("PARSED: %s" % actualitem)
if len(actualitem) > 0:
for replace in actualitem:
try:
to_be_replaced = replace[0]
except IndexError:
continue
value = get_json_value(fullexecution, to_be_replaced)
if isinstance(value, str):
parameter["value"] = parameter["value"].replace(to_be_replaced, value)
elif isinstance(value, dict):
parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
else:
print("Unknown type %s" % type(value))
try:
parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
except json.decoder.JSONDecodeError as e:
parameter["value"] = parameter["value"].replace(to_be_replaced, value)
if parameter["variant"] == "WORKFLOW_VARIABLE":
for item in fullexecution["workflow"]["workflow_variables"]:
if parameter["action_field"] == item["name"]:
parameter["value"] = item["value"]
break
elif parameter["variant"] == "ACTION_RESULT":
# FIXME - calculate value based on action_field and $if prominent
# FIND THE RIGHT LABEL
# GET THE LABEL'S RESULT
tmpvalue = ""
self.logger.info("ACTION FIELD: %s" % parameter["action_field"])
#"$%s%s" %
fullname = "$"
if parameter["action_field"] == "Execution Argument":
tmpvalue = fullexecution["execution_argument"]
fullname += "exec"
else:
fullname += parameter["action_field"]
if parameter["value"].startswith(jsonparsevalue):
fullname += parameter["value"][2:]
else:
fullname = "$%s" % parameter["action_field"]
self.logger.info("Fullname: %s" % fullname)
actualitem = re.findall(match, fullname, re.MULTILINE)
self.logger.info("PARSED: %s" % actualitem)
if len(actualitem) > 0:
for replace in actualitem:
try:
to_be_replaced = replace[0]
except IndexError:
print("Nothing to replace?: " % e)
continue
# This will never be a loop aka multi argument
parameter["value"] = to_be_replaced
value = get_json_value(fullexecution, to_be_replaced)
if isinstance(value, str):
parameter["value"] = parameter["value"].replace(to_be_replaced, value)
elif isinstance(value, dict):
parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
else:
print("Unknown type %s" % type(value))
try:
parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
except json.decoder.JSONDecodeError as e:
parameter["value"] = parameter["value"].replace(to_be_replaced, value)
return "", parameter["value"]
def run_validation(sourcevalue, check, destinationvalue):
self.logger.info("Checking %s %s %s" % (sourcevalue, check, destinationvalue))
if check == "=" or check.lower() == "equals":
if sourcevalue.lower() == destinationvalue.lower():
return True
elif check == "!=" or check.lower() == "does not equal":
if sourcevalue.lower() != destinationvalue.lower():
return True
elif check.lower() == "startswith":
if sourcevalue.lower().startswith(destinationvalue.lower()):
return True
elif check.lower() == "endswith":
if sourcevalue.lower().endswith(destinationvalue.lower()):
return True
elif check.lower() == "contains":
if destinationvalue.lower() in sourcevalue.lower():
return True
else:
self.logger.info("Condition: can't handle %s yet. Setting to true" % check)
return False
def check_branch_conditions(action, fullexecution):
# relevantbranches = workflow.branches where destination = action
try:
if fullexecution["workflow"]["branches"] == None or len(fullexecution["workflow"]["branches"]) == 0:
return True, ""
except KeyError:
return True, ""
relevantbranches = []
for branch in fullexecution["workflow"]["branches"]:
if branch["destination_id"] != action["id"]:
continue
self.logger.info("Relevant branch: %s" % branch)
# Remove anything without a condition
try:
if (branch["conditions"]) == 0 or branch["conditions"] == None:
continue
except KeyError:
continue
self.logger.info("Relevant conditions: %s" % branch["conditions"])
successful_conditions = []
failed_conditions = []
for condition in branch["conditions"]:
self.logger.info("Getting condition value of %s" % condition)
# Parse all values first here
sourcevalue = condition["source"]["value"]
if condition["source"]["variant"] == "" or condition["source"]["variant"]== "STATIC_VALUE":
condition["source"]["variant"]= "STATIC_VALUE"
else:
check, sourcevalue = parse_params(action, fullexecution, condition["source"])
if check:
return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)
print(sourcevalue)
destinationvalue = condition["destination"]["value"]
if condition["destination"]["variant"]== "" or condition["destination"]["variant"]== "STATIC_VALUE":
condition["destination"]["variant"] = "STATIC_VALUE"
else:
check, destinationvalue = parse_params(action, fullexecution, condition["destination"])
if check:
return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)
available_checks = [
"=",
"equals",
"!=",
"does not equal",
">",
"larger than",
"<",
"less than",
">=",
"<=",
"startswith",
"endswith",
"contains",
"re",
"matches regex",
]
# FIXME - what should I do here?
if not condition["condition"]["value"] in available_checks:
self.logger.info("Skipping %s %s %s because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"]))
continue
#print(destinationvalue)
if not run_validation(sourcevalue, condition["condition"]["value"], destinationvalue):
self.logger.info("Failed condition check for %s %s %s." % (sourcevalue, condition["condition"]["value"], destinationvalue))
return False, "Failed condition: %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue)
# Make a general parser here, at least to get param["name"] = param["value"] in maparameter[string]string
#for condition in branch.conditons:
return True, ""
# Checks whether conditions are met, otherwise set
branchcheck, tmpresult = check_branch_conditions(action, fullexecution)
if not branchcheck:
self.logger.info("Failed one or more branch conditions.")
action_result["result"] = tmpresult
action_result["status"] = "SKIPPED"
try:
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Result: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
except requests.exceptions.ConnectionError as e:
self.logger.exception(e)
return
# Replace name cus there might be issues
# Not doing lower() as there might be user-made functions
actionname = action["name"]
if " " in actionname:
actionname.replace(" ", "_", -1)
#if action.generated:
# actionname = actionname.lower()
# Runs the actual functions
try:
func = getattr(self, actionname, None)
if func == None:
self.logger.debug("Failed executing %s because func is None." % actionname)
action_result["status"] = "FAILURE"
action_result["result"] = "Function %s doesn't exist." % actionname
elif callable(func):
try:
if len(action["parameters"]) < 1:
result = await func()
else:
# Potentially parse JSON here
# FIXME - add potential authentication as first parameter(s) here
# params[parameter["name"]] = parameter["value"]
#print(fullexecution["authentication"]
# What variables are necessary here tho hmm
params = {}
try:
for item in action["authentication"]:
print("AUTH: ", key, value)
params[item["key"]] = item["value"]
except KeyError:
print("No authentication specified!")
pass
#action["authentication"]
# calltimes is used to handle forloops in the app itself.
# 2 kinds of loop - one in gui with one app each, and one like this,
# which is super fast, but has a bad overview (potentially good tho)
calltimes = 1
result = ""
all_executions = []
# Multi_parameter has the data for each. variable
minlength = 0
multi_parameters = json.loads(json.dumps(params))
multiexecution = False
for parameter in action["parameters"]:
check, value = parse_params(action, fullexecution, parameter)
if check:
raise Exception(check)
# Custom format for ${name[0,1,2,...]}$
submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})"
actualitem = re.findall(submatch, value, re.MULTILINE)
if len(actualitem) > 0:
multiexecution = True
# This is here to handle for loops within variables.. kindof
# 1. Find the length of the longest array
# 2. Build an array with the base values based on parameter["value"]
# 3. Get the n'th value of the generated list from values
# 4. Execute all n answers
replacements = {}
for replace in actualitem:
try:
to_be_replaced = replace[0]
actualitem = replace[2]
except IndexError:
continue
itemlist = json.loads(actualitem)
if len(itemlist) > minlength:
minlength = len(itemlist)
replacements[to_be_replaced] = actualitem
# This is a result array for JUST this value..
# What if there are more?
resultarray = []
for i in range(0, minlength):
tmpitem = json.loads(json.dumps(parameter["value"]))
for key, value in replacements.items():
replacement = json.loads(value)[i]
tmpitem = tmpitem.replace(key, replacement, -1)
resultarray.append(tmpitem)
# With this parameter ready, add it to... a greater list of parameters. Rofl
multi_parameters[parameter["name"]] = resultarray
else:
print("Hello, in here?: %s" % value)
params[parameter["name"]] = value
multi_parameters[parameter["name"]] = value
# FIXME - this is horrible, but works for now
#for i in range(calltimes):
if not multiexecution:
print("Params: %s" % params)
print("RUNNING NORMAL EXECUTION")
result += await func(**params)
else:
print("MULTI EXECUTION: ", multi_parameters)
# 1. Use number of executions based on longest array
# 2. Find the right value from the parsed multi_params
results = []
json_object = False
for i in range(0, minlength):
# To be able to use the results as a list:
baseparams = json.loads(json.dumps(multi_parameters))
try:
for key, value in baseparams.items():
if isinstance(value, list):
baseparams[key] = value[i]
except IndexError as e:
print("IndexError: %s" % e)
baseparams[key] = "IndexError: %s" % e
except KeyError as e:
print("KeyError: %s" % e)
baseparams[key] = "KeyError: %s" % e
#print("Running with params %s" % baseparams)
ret = await func(**baseparams)
print("Inner ret: %s" % ret)
try:
results.append(json.loads(ret))
json_object = True
except json.decoder.JSONDecodeError as e:
results.append(ret)
# Dump the result as a string of a list
print("RESULTS: %s" % results)
if isinstance(results, list):
print("JSON OBJECT? ", json_object)
if json_object:
result = json.dumps(results)
else:
result = "[\""+"\", \"".join(results)+"\"]"
else:
print("Normal result?")
result = results
print("RESULT: %s" % result)
action_result["status"] = "SUCCESS"
action_result["result"] = str(result)
if action_result["result"] == "":
action_result["result"] = result
self.logger.debug(f"Executed {action['label']}-{action['id']} with result: {result}")
self.logger.debug(f"Data: %s" % action_result)
except TypeError as e:
action_result["status"] = "FAILURE"
action_result["result"] = "TypeError: %s" % str(e)
else:
print("Not callable?")
self.logger.error(f"App {self.__class__.__name__}.{action['name']} is not callable")
action_result["status"] = "FAILURE"
action_result["result"] = "Function %s is not callable." % actionname
except Exception as e:
print(f"Failed to execute: {e}")
self.logger.exception(f"Failed to execute {e}-{action['id']}")
action_result["status"] = "FAILURE"
action_result["result"] = "Exception: %s" % e
action_result["completed_at"] = int(time.time())
# I wonder if this actually works
self.logger.info("Before last stream result")
try:
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Result: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
except requests.exceptions.ConnectionError as e:
self.logger.exception(e)
return
except TypeError as e:
self.logger.exception(e)
action_result["status"] = "FAILURE"
action_result["result"] = "POST error: %s" % e
self.logger.info("Before typeerror stream result")
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Result: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
return
#STOPCOPY
# !!! Let the above line stay - its used for some horrible codegeneration / stitching !!! #
@classmethod
async def run(cls):
""" Connect to Redis and HTTP session, await actions """
logging.basicConfig(format="{asctime} - {name} - {levelname}:{message}", style='{')
logger = logging.getLogger(f"{cls.__name__}")
logger.setLevel(logging.DEBUG)
app = cls(redis=None, logger=logger, console_logger=logger)
# Authorization for the app/function to control the workflow
# Function will crash if its wrong, which it probably should.
await app.execute_action(app.action)
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
docker rmi frikky/shuffle:app_sdk
docker build . -t frikky/shuffle:app_sdk --no-cache
docker push frikky/shuffle:app_sdk
+2
View File
@@ -0,0 +1,2 @@
requests
urllib3
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
docker stop shuffle-backend
docker rm shuffle-backend
docker rmi frikky/shuffle:backend
docker build . -t frikky/shuffle:backend
docker push frikky/shuffle:backend
echo "Starting server"
#docker run -it \
# -p 5001:5001 \
# -v /var/run/docker.sock:/var/run/docker.sock \
# --env DATASTORE_EMULATOR_HOST=192.168.3.6:8000 \
# frikky/shuffle:backend
-8
View File
@@ -1,8 +0,0 @@
# Deploys to backend
echo "Deploying to appengine."
mkdir -p $GOPATH/src/github.com/frikky/shuffle
cp -r go-app/* $GOPATH/src/github.com/frikky/shuffle
cd $GOPATH/src/github.com/frikky/shuffle
go build
go test
gcloud app deploy $GOPATH/src/github.com/frikky/shuffle/app.yaml
+31 -9
View File
@@ -122,8 +122,8 @@ func streamZipdata(ctx context.Context, identifier, pythoncode, requirements str
func getAppbase() ([]byte, []byte, error) {
// 1. Have baseline in bucket/generated_apps/baseline
// 2. Copy the baseline to a new folder with identifier name
static := "../../functions/static_baseline.py"
appbase := "../../functions/onprem/app_sdk/app_base.py"
static := "../app_sdk/static_baseline.py"
appbase := "../app_sdk/app_base.py"
staticData, err := ioutil.ReadFile(static)
if err != nil {
@@ -218,7 +218,7 @@ func buildStructure(swagger *openapi3.Swagger, curHash string) (string, error) {
// adding md5 based on input data to not overwrite earlier data.
generatedPath := "generated"
subpath := "../../app_gen/openapi/"
subpath := "../app_gen/openapi/"
identifier := fmt.Sprintf("%s-%s", swagger.Info.Title, curHash)
appPath := fmt.Sprintf("%s/%s", generatedPath, identifier)
@@ -361,12 +361,13 @@ func generateYaml(swagger *openapi3.Swagger, newmd5 string) (WorkflowApp, []stri
api.Tested = false
api.PrivateID = newmd5
api.Generated = true
api.Activated = true
// Setting up security schemes
extraParameters := []WorkflowAppActionParameter{}
securitySchemes := swagger.Components.SecuritySchemes
if securitySchemes != nil {
log.Printf("%#v", securitySchemes)
//log.Printf("%#v", securitySchemes)
api.Authentication = Authentication{
Required: true,
@@ -386,7 +387,7 @@ func generateYaml(swagger *openapi3.Swagger, newmd5 string) (WorkflowApp, []stri
api.Authentication.Parameters[0].Name = securitySchemes["BearerAuth"].Value.Name
api.Authentication.Parameters[0].In = securitySchemes["BearerAuth"].Value.In
api.Authentication.Parameters[0].Scheme = securitySchemes["BearerAuth"].Value.Scheme
log.Printf("HANDLE BEARER AUTH")
//log.Printf("HANDLE BEARER AUTH")
extraParameters = append(extraParameters, WorkflowAppActionParameter{
Name: "apikey",
Description: "The apikey to use",
@@ -402,7 +403,7 @@ func generateYaml(swagger *openapi3.Swagger, newmd5 string) (WorkflowApp, []stri
api.Authentication.Parameters[0].Name = securitySchemes["ApiKeyAuth"].Value.Name
api.Authentication.Parameters[0].In = securitySchemes["ApiKeyAuth"].Value.In
api.Authentication.Parameters[0].Scheme = securitySchemes["ApiKeyAuth"].Value.Scheme
log.Printf("HANDLE APIKEY AUTH")
//log.Printf("HANDLE APIKEY AUTH")
extraParameters = append(extraParameters, WorkflowAppActionParameter{
Name: "apikey",
Description: "The apikey to use",
@@ -704,6 +705,9 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [
optionalParameters := []WorkflowAppActionParameter{}
if len(path.Connect.Parameters) > 0 {
for _, param := range path.Connect.Parameters {
if param.Value.Schema == nil {
continue
}
curParam := WorkflowAppActionParameter{
Name: param.Value.Name,
Description: param.Value.Description,
@@ -773,8 +777,6 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor
Parameters: extraParameters,
}
log.Printf("FUNCTION: %#v", action)
action.Returns.Schema.Type = "string"
baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath)
@@ -791,6 +793,11 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor
optionalParameters := []WorkflowAppActionParameter{}
if len(path.Get.Parameters) > 0 {
for _, param := range path.Get.Parameters {
//log.Printf("TYPE: %#v", param.Value.Schema)
if param.Value.Schema == nil {
continue
}
curParam := WorkflowAppActionParameter{
Name: param.Value.Name,
Description: param.Value.Description,
@@ -873,6 +880,9 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo
optionalParameters := []WorkflowAppActionParameter{}
if len(path.Head.Parameters) > 0 {
for _, param := range path.Head.Parameters {
if param.Value.Schema == nil {
continue
}
curParam := WorkflowAppActionParameter{
Name: param.Value.Name,
Description: param.Value.Description,
@@ -955,6 +965,9 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []
optionalParameters := []WorkflowAppActionParameter{}
if len(path.Delete.Parameters) > 0 {
for _, param := range path.Delete.Parameters {
if param.Value.Schema == nil {
continue
}
curParam := WorkflowAppActionParameter{
Name: param.Value.Name,
Description: param.Value.Description,
@@ -1013,7 +1026,7 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []
func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, firstQuery bool) (WorkflowAppAction, string) {
// What to do with this, hmm
log.Printf("PATH: %s", actualPath)
//log.Printf("PATH: %s", actualPath)
functionName := fixFunctionName(path.Post.Summary, actualPath)
action := WorkflowAppAction{
@@ -1049,6 +1062,9 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo
}
if len(path.Post.Parameters) > 0 {
for _, param := range path.Post.Parameters {
if param.Value.Schema == nil {
continue
}
curParam := WorkflowAppActionParameter{
Name: param.Value.Name,
Description: param.Value.Description,
@@ -1142,6 +1158,9 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W
}
if len(path.Patch.Parameters) > 0 {
for _, param := range path.Patch.Parameters {
if param.Value.Schema == nil {
continue
}
curParam := WorkflowAppActionParameter{
Name: param.Value.Name,
Description: param.Value.Description,
@@ -1235,6 +1254,9 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor
}
if len(path.Put.Parameters) > 0 {
for _, param := range path.Put.Parameters {
if param.Value.Schema == nil {
continue
}
curParam := WorkflowAppActionParameter{
Name: param.Value.Name,
Description: param.Value.Description,
+141 -10
View File
@@ -5148,6 +5148,121 @@ func echoOpenapiData(resp http.ResponseWriter, request *http.Request) {
resp.Write(urlbody)
}
func handleSwaggerValidation(body []byte) (ParsedOpenApi, error) {
type versionCheck struct {
Swagger string `datastore:"swagger" json:"swagger" yaml:"swagger"`
SwaggerVersion string `datastore:"swaggerVersion" json:"swaggerVersion" yaml:"swaggerVersion"`
OpenAPI string `datastore:"openapi" json:"openapi" yaml:"openapi"`
}
//body = []byte(`swagger: "2.0"`)
//body = []byte(`swagger: '1.0'`)
//newbody := string(body)
//newbody = strings.TrimSpace(newbody)
//body = []byte(newbody)
//log.Println(string(body))
//tmpbody, err := yaml.YAMLToJSON(body)
//log.Println(err)
//log.Println(string(tmpbody))
// This has to be done in a weird way because Datastore doesn't
// support map[string]interface and similar (openapi3.Swagger)
var version versionCheck
parsed := ParsedOpenApi{}
swaggerdata := []byte{}
idstring := ""
isJson := false
err := json.Unmarshal(body, &version)
if err != nil {
//log.Printf("Json err: %s", err)
err = yaml.Unmarshal(body, &version)
if err != nil {
log.Printf("Yaml error: %s", err)
} else {
//log.Printf("Successfully parsed YAML!")
}
} else {
isJson = true
log.Printf("Successfully parsed JSON!")
}
if len(version.SwaggerVersion) > 0 && len(version.Swagger) == 0 {
version.Swagger = version.SwaggerVersion
}
if strings.HasPrefix(version.Swagger, "3.") || strings.HasPrefix(version.OpenAPI, "3.") {
//log.Println("Handling v3 API")
swaggerv3, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(body)
if err != nil {
return ParsedOpenApi{}, err
}
swaggerdata, err = json.Marshal(swaggerv3)
if err != nil {
log.Printf("Failed unmarshaling v3 data: %s", err)
return ParsedOpenApi{}, err
}
hasher := md5.New()
hasher.Write(swaggerdata)
idstring = hex.EncodeToString(hasher.Sum(nil))
} else { //strings.HasPrefix(version.Swagger, "2.") || strings.HasPrefix(version.OpenAPI, "2.") {
// Convert
//log.Println("Handling v2 API")
var swagger openapi2.Swagger
//log.Println(string(body))
err = json.Unmarshal(body, &swagger)
if err != nil {
//log.Printf("Json error? %s", err)
err = gyaml.Unmarshal(body, &swagger)
if err != nil {
log.Printf("Yaml error: %s", err)
return ParsedOpenApi{}, err
} else {
//log.Printf("Valid yaml!")
}
}
swaggerv3, err := openapi2conv.ToV3Swagger(&swagger)
if err != nil {
log.Printf("Failed converting from openapi2 to 3: %s", err)
return ParsedOpenApi{}, err
}
swaggerdata, err = json.Marshal(swaggerv3)
if err != nil {
log.Printf("Failed unmarshaling v3 data: %s", err)
return ParsedOpenApi{}, err
}
hasher := md5.New()
hasher.Write(swaggerdata)
idstring = hex.EncodeToString(hasher.Sum(nil))
}
if len(swaggerdata) > 0 {
body = swaggerdata
}
// Overwrite with new json data
_ = isJson
body = swaggerdata
// Parsing it to swagger 3
parsed = ParsedOpenApi{
ID: idstring,
Body: string(body),
Success: true,
}
return parsed, err
}
// FIXME: Migrate this to use handleSwaggerValidation()
func validateSwagger(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
if cors {
@@ -5418,7 +5533,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) {
return
}
log.Printf("Should generate yaml")
//log.Printf("Should generate yaml")
api, pythonfunctions, err := generateYaml(swagger, newmd5)
if err != nil {
log.Printf("Failed building and generating yaml: %s", err)
@@ -5569,14 +5684,6 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) {
return
}
log.Println(len(user.PrivateApps))
c, err := request.Cookie("session_token")
if err == nil {
log.Printf("Should've deleted cache for %s with token %s", user.Username, c.Value)
//err = memcache.Delete(request.Context(), c.Value)
//err = memcache.Delete(request.Context(), user.ApiKey)
}
parsed := ParsedOpenApi{
ID: api.ID,
Body: string(body),
@@ -5656,7 +5763,7 @@ func runInit(ctx context.Context) {
if err != nil {
log.Printf("Failed getting apps: %s", err)
} else if err == nil && len(workflowapps) == 0 {
log.Printf("Apps: loading TEST")
log.Printf("Downloading default workflow apps")
fs := memfs.New()
storer := memory.NewStorage()
@@ -5696,6 +5803,30 @@ func runInit(ctx context.Context) {
iterateAppGithubFolders(fs, dir, "", "")
}
log.Printf("Downloading OpenAPI data for search - EXTRA APPS")
apis := "https://github.com/frikky/OpenAPI-security-definitions"
// THis gets memory problems hahah
//apis := "https://github.com/APIs-guru/openapi-directory"
fs := memfs.New()
storer := memory.NewStorage()
cloneOptions := &git.CloneOptions{
URL: apis,
}
_, err = git.Clone(storer, fs, cloneOptions)
if err != nil {
log.Printf("Failed loading repo %s into memory: %s", err)
} else {
log.Printf("Finished git clone. Looking for updates to the repo.")
dir, err := fs.ReadDir("")
if err != nil {
log.Printf("Failed reading folder: %s", err)
}
iterateOpenApiGithub(fs, dir, "", "")
log.Printf("Finished downloading extra API samples")
}
log.Printf("Finished INIT")
}
+174 -10
View File
@@ -29,6 +29,7 @@ import (
http2 "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
newscheduler "github.com/carlescere/scheduler"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-git/go-git/v5/storage/memory"
//"github.com/gorilla/websocket"
//"google.golang.org/appengine"
@@ -85,10 +86,11 @@ type WorkflowApp struct {
Downloaded bool `json:"downloaded" yaml:"downloaded" required:false datastore:"downloaded"`
Sharing bool `json:"sharing" yaml:"sharing" required:false datastore:"sharing"`
Verified bool `json:"verified" yaml:"verified" required:false datastore:"verified"`
Activated bool `json:"activated" yaml:"activated" required:false datastore:"activated"`
Tested bool `json:"tested" yaml:"tested" required:false datastore:"tested"`
Owner string `json:"owner" datastore:"owner" yaml:"owner"`
PrivateID string `json:"private_id" yaml:"private_id" required:false datastore:"private_id"`
Description string `json:"description" datastore:"description" required:false yaml:"description"`
Description string `json:"description" datastore:"description,noindex" required:false yaml:"description"`
Environment string `json:"environment" datastore:"environment" required:true yaml:"environment"`
SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"`
LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false`
@@ -636,10 +638,6 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
return
}
//for _, action := range workflowExecution.Workflow.Actions {
// log.Printf("Name: %s, Env: %s", action.Name, action.Environment)
//}
newjson, err := json.Marshal(workflowExecution)
if err != nil {
resp.WriteHeader(401)
@@ -2702,7 +2700,6 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) {
}
location := strings.Split(request.URL.String(), "/")
log.Printf("%#v", location)
var fileId string
if location[1] == "api" {
if len(location) <= 4 {
@@ -2730,14 +2727,22 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) {
return
}
log.Printf("Getting app %s", fileId)
parsedApi, err := getOpenApiDatastore(ctx, fileId)
if err != nil {
log.Printf("OpenApi doesn't exist for: %s - err: %s", fileId, err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
parsedApi.Success = true
//log.Printf("Parsed API: %#v", parsedApi)
if len(parsedApi.ID) > 0 {
parsedApi.Success = true
} else {
parsedApi.Success = false
}
data, err := json.Marshal(parsedApi)
if err != nil {
resp.WriteHeader(422)
@@ -2917,6 +2922,7 @@ func handleGetfile(resp http.ResponseWriter, request *http.Request) ([]byte, err
return buf.Bytes(), nil
}
// Basically a search for apps that aren't activated yet
func getSpecificApps(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
if cors {
@@ -2956,10 +2962,40 @@ func getSpecificApps(resp http.ResponseWriter, request *http.Request) {
// FIXME - continue the search here with github repos etc.
// Caching might be smart :D
log.Printf("Body: %s", string(body))
ctx := context.Background()
workflowapps, err := getAllWorkflowApps(ctx)
if err != nil {
log.Printf("Error: Failed getting workflowapps: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
returnValues := []WorkflowApp{}
search := strings.ToLower(tmpBody.Search)
for _, app := range workflowapps {
if !app.Activated && app.Generated {
// This might be heavy with A LOT
// Not too worried with todays tech tbh..
appName := strings.ToLower(app.Name)
appDesc := strings.ToLower(app.Description)
if strings.Contains(appName, search) || strings.Contains(appDesc, search) {
//log.Printf("Name: %s, Generated: %s, Activated: %s", app.Name, strconv.FormatBool(app.Generated), strconv.FormatBool(app.Activated))
returnValues = append(returnValues, app)
}
}
}
newbody, err := json.Marshal(returnValues)
if err != nil {
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unpacking workflow executions"}`)))
return
}
returnData := fmt.Sprintf(`{"success": true, "reason": %s}`, string(newbody))
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
resp.Write([]byte(returnData))
}
func validateAppInput(resp http.ResponseWriter, request *http.Request) {
@@ -3257,6 +3293,133 @@ func loadSpecificApps(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
}
func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string) error {
ctx := context.Background()
workflowapps, err := getAllWorkflowApps(ctx)
appCounter := 0
if err != nil {
log.Printf("Failed to get existing generated apps")
}
for _, file := range dir {
if len(onlyname) > 0 && file.Name() != onlyname {
continue
}
// Folder?
switch mode := file.Mode(); {
case mode.IsDir():
tmpExtra := fmt.Sprintf("%s%s/", extra, file.Name())
dir, err := fs.ReadDir(tmpExtra)
if err != nil {
log.Printf("Failed to read dir: %s", err)
break
}
// Go routine? Hmm, this can be super quick I guess
err = iterateOpenApiGithub(fs, dir, tmpExtra, "")
if err != nil {
break
}
case mode.IsRegular():
// Check the file
filename := file.Name()
if strings.Contains(filename, "yaml") || strings.Contains(filename, "yml") {
appCounter += 1
//log.Printf("File: %s", filename)
//log.Printf("Found file: %s", filename)
tmpExtra := fmt.Sprintf("%s%s/", extra, file.Name())
fileReader, err := fs.Open(tmpExtra)
if err != nil {
continue
}
readFile, err := ioutil.ReadAll(fileReader)
if err != nil {
log.Printf("Filereader error yaml: %s", err)
continue
}
// 1. This parses OpenAPI v2 to v3 etc, for use.
parsedOpenApi, err := handleSwaggerValidation(readFile)
if err != nil {
log.Printf("Validation error: %s", err)
continue
}
// 2. With parsedOpenApi.ID:
//http://localhost:3000/apps/new?id=06b1376f77b0563a3b1747a3a1253e88
// 3. Load this as a "standby" app
// FIXME: This should be a function ROFL
//log.Printf("%s", string(readFile))
swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(parsedOpenApi.Body))
if err != nil {
log.Printf("Swagger validation error in loop (%s): %s", filename, err)
continue
}
if strings.Contains(swagger.Info.Title, " ") {
strings.Replace(swagger.Info.Title, " ", "", -1)
}
//log.Printf("Should generate yaml")
api, _, err := generateYaml(swagger, parsedOpenApi.ID)
if err != nil {
log.Printf("Failed building and generating yaml in loop (%s): %s", filename, err)
continue
}
// FIXME: Configure user?
api.Owner = ""
api.ID = parsedOpenApi.ID
api.IsValid = true
api.Generated = true
api.Activated = false
found := false
for _, app := range workflowapps {
if app.ID == api.ID {
found = true
break
} else if app.Name == api.Name && app.AppVersion == api.AppVersion {
found = true
break
}
}
if !found {
err = setWorkflowAppDatastore(ctx, api, api.ID)
if err != nil {
log.Printf("Failed setting workflowapp in loop: %s", err)
continue
} else {
log.Printf("Added %s:%s to the database from OpenAPI repo", api.Name, api.AppVersion)
// Set OpenAPI datastore
err = setOpenApiDatastore(ctx, parsedOpenApi.ID, parsedOpenApi)
if err != nil {
log.Printf("Failed uploading openapi to datastore in loop: %s", err)
continue
}
}
} else {
//log.Printf("Skipped upload of %s (%s)", api.Name, api.ID)
}
//return nil
}
}
}
if appCounter > 0 {
log.Printf("Preloaded %d OpenApi apps in %s!", appCounter, extra)
}
return nil
}
// Onlyname is used to
func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string) error {
var err error
@@ -3475,6 +3638,7 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) {
workflowapp.ID = uuid.NewV4().String()
workflowapp.IsValid = true
workflowapp.Generated = false
workflowapp.Activated = true
err = setWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID)
if err != nil {
@@ -3543,7 +3707,7 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) {
}
// Query for the specifci workflowId
q := datastore.NewQuery("workflowexecution").Filter("workflow_id =", fileId).Limit(50)
q := datastore.NewQuery("workflowexecution").Filter("workflow_id =", fileId).Order("-started_at").Limit(50)
var workflowExecutions []WorkflowExecution
_, err = dbclient.GetAll(ctx, q, &workflowExecutions)
if err != nil {
-15
View File
@@ -1,15 +0,0 @@
#!/bin/sh
# docker stop nginx
# docker rm nginx
# docker rmi nginx
#
# echo "Running build for website"
# sudo npm run build
# docker build . -t nginx
echo "Starting server"
docker run -it \
-p 5001:5001 \
-v /var/run/docker.sock:/var/run/docker.sock \
--env DATASTORE_EMULATOR_HOST=192.168.3.6:8000 \
frikky/shuffle:backend
-15
View File
@@ -1,15 +0,0 @@
# Build environment
# production environment
from golang as builder
RUN go get github.com/gorilla/handlers
RUN go get github.com/gorilla/mux
WORKDIR /app
COPY webhook.go /app/webhook.go
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook .
from scratch
COPY --from=builder /app/ /
CMD ["./webhook"]
-7
View File
@@ -1,7 +0,0 @@
# Steps to deploy to Google cloud function
1.
```bash
zip webhook.zip *
```
2. Go to google cloud bucket and upload the zip
3. Go to worker for webhook and upload
-44
View File
@@ -1,44 +0,0 @@
package function
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"time"
)
// GetUserDetails - Get one user's details from randomuser.me API
func GetUserDetails(w http.ResponseWriter, r *http.Request) {
randomUserClient := http.Client{
Timeout: time.Second * 3,
}
req, err := http.NewRequest(http.MethodGet, "https://randomuser.me/api/", nil)
if err != nil {
log.Fatal(err)
return
}
res, err2 := randomUserClient.Do(req)
if err2 != nil {
log.Fatal(err2)
return
}
body, err3 := ioutil.ReadAll(res.Body)
if err3 != nil {
log.Fatal(err3)
}
var o map[string]interface{}
json.Unmarshal([]byte(body), &o)
results := o["results"].([]interface{})
result := results[0].(map[string]interface{})
result["generator"] = "google-cloud-function"
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
-4
View File
@@ -1,4 +0,0 @@
docker build . -t gcr.io/shuffle-241517/webhook
docker push gcr.io/shuffle-241517/webhook
gcloud beta run deploy webhook --image gcr.io/shuffle-241517/webhook
-16
View File
@@ -1,16 +0,0 @@
docker stop webhook
docker rm webhook
docker rmi webhook
docker build . -t webhook
docker run -d \
-e "HOOKPORT=5001" \
-e "URIPATH=/webhook" \
-e "CALLBACKURL=http://192.168.3.6:5001/api/v1/hooks/d6ef8912e8bd37776e654cbc14c2629c/result" \
-p 6000:6000 \
--name webhook \
-h webhook \
--restart always \
webhook
docker logs -f webhook
-260
View File
@@ -1,260 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
type Info struct {
Url string `json:"url" datastore:"url"`
Name string `json:"name" datastore:"name"`
Description string `json:"description" datastore:"description"`
}
// Actions to be done by webhooks etc
// Field is the actual field to use from json
type HookAction struct {
Type string `json:"type" datastore:"type"`
Name string `json:"name" datastore:"name"`
Id string `json:"id" datastore:"id"`
Field string `json:"field" datastore:"field"`
}
type Hook struct {
Id string `json:"id" datastore:"id"`
Info Info `json:"info" datastore:"info"`
Transforms struct{} `json:"transforms" datastore:"transforms"`
Actions []HookAction `json:"actions" datastore:"actions"`
Type string `json:"type" datastore:"type"`
Status string `json:"status" datastore:"status"`
Running bool `json:"running" datastore:"running"`
}
var hook Hook
func handleWorkflowAction(request *http.Request, action HookAction) error {
//log.Printf("WORKFLOW!: %#v", action)
log.Printf("Should execute workflow %s", action.Id)
callbackUrl := os.Getenv("CALLBACKURL")
apikey := os.Getenv("APIKEY")
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", callbackUrl, action.Id)
// ret = requests.post(fullurl, headers=headers, json=data)
//if ret.status_code != 202:
// print(ret.text)
// print(ret.status_code)
// print("Exiting workflows - run queue")
// exit()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return err
}
// Execute a workflow
client := &http.Client{}
req, err := http.NewRequest(
"POST",
fullUrl,
bytes.NewBuffer(body),
)
if err != nil {
log.Printf("Error making http request: %s", req)
return err
}
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("Error in http request: %s", req)
}
log.Printf("%#v", resp)
return nil
}
// FIXME - refresh hook information once in a while. Compare timestamps or something
func callback(resp http.ResponseWriter, request *http.Request) {
//apikey = os.Getenv("APIKEY")
//hookId = os.Getenv("HOOKID")
handledWorkflowIds := []string{}
for _, item := range hook.Actions {
if item.Type == "" {
log.Printf("CONTINUE AAS EMPTY ITEM: %#v", item)
continue
}
if item.Type == "workflow" {
found := false
for _, workflowId := range handledWorkflowIds {
if item.Id == workflowId {
found = true
break
}
}
if found {
continue
}
handledWorkflowIds = append(handledWorkflowIds, item.Id)
err := handleWorkflowAction(request, item)
if err != nil {
log.Printf("Error in workflow exec: %s", err)
}
}
}
// FIXME - send the webhookdata to a logging service? Idk
//body, err := ioutil.ReadAll(request.Body)
//if err != nil {
// log.Println("Failed reading body")
// resp.WriteHeader(401)
// resp.Write([]byte(fmt.Sprintf(`{"success": false}`)))
// return
//}
//callback, err := http.Post(callbackUrl, "application/json", bytes.NewBuffer(body))
//if err != nil {
// log.Printf("Failed sending callback to %s", callbackUrl)
//}
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
return
}
func loadConfiguration(fullUrl string, apikey string) error {
client := &http.Client{}
req, err := http.NewRequest(
"GET",
fullUrl,
nil,
)
if err != nil {
log.Printf("Error making http request: %s", req)
return err
}
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("Error in http request: %s", req)
return err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response: %s", req)
return err
}
err = json.Unmarshal(body, &hook)
if err != nil {
log.Printf("Failed unmarshaling hook API", req)
return err
}
log.Printf("%#v", hook)
log.Println(hook.Actions)
return nil
}
func webhook() {
// FIXME - remove static
ip := "0.0.0.0"
// Basic webserver stuff
baseFilePath := os.Getenv("URIPATH")
basePort := os.Getenv("HOOKPORT")
callbackUrl := os.Getenv("CALLBACKURL")
apikey := os.Getenv("APIKEY")
hookId := os.Getenv("HOOKID")
if len(baseFilePath) == 0 {
log.Println("Env URIPATH not set")
os.Exit(3)
}
if len(basePort) == 0 {
log.Println("Env HOOKPORT not set")
os.Exit(3)
}
if len(callbackUrl) == 0 {
log.Println("Env CALLBACKURL not set")
os.Exit(3)
}
if len(apikey) == 0 {
log.Println("Env APIKEY not set")
os.Exit(3)
}
if len(hookId) == 0 {
log.Println("Env HOOKID not set")
os.Exit(3)
}
log.Println("Loading hook configuration")
err := loadConfiguration(
fmt.Sprintf("%s/api/v1/hooks/%s", callbackUrl, hookId),
apikey,
)
if err != nil {
log.Fatalf("Error loading config: %s", err)
}
// Optional
// if len(callbackOpts) == 0 {
// log.Println("Env CALLBACKOPTS not set")
// os.Exit(3)
// }
port := fmt.Sprintf(":%s", basePort)
log.Printf("Starting webhook on %s%s with path %s", ip, port, baseFilePath)
// Routing
mux := mux.NewRouter()
mux.SkipClean(true)
// FIXME - Add path for updating the hook? Can be a specific POST requeuest from backend
mux.HandleFunc(baseFilePath, callback).Methods("POST")
handlers.LoggingHandler(os.Stdout, mux)
loggedRouter := handlers.LoggingHandler(os.Stdout, mux)
err = http.ListenAndServe(
port,
loggedRouter,
)
if err != nil {
log.Fatal("ListenAndServer: ", err)
}
}
func F(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(r.Header.Get("X-Forwarded-For")))
}
func main() {
webhook()
}
+9 -9
View File
@@ -1,17 +1,17 @@
#!/bin/sh
docker stop frikky/shuffle:frontend
docker rm frikky/shuffle:frontend
docker stop shuffle-frontend
docker rm shuffle-frontend
docker rmi frikky/shuffle:frontend
echo "Running build for website"
sudo npm run build
#sudo npm run build
docker build . -t frikky/shuffle:frontend
echo "Starting server"
# Rerun build locally for it to update :)
docker run -it \
-p 3001:80 \
-p 3002:443 \
-v $(pwd)/build:/usr/share/nginx/html:ro \
--rm \
nginx
#docker run -it \
# -p 3001:80 \
# -p 3002:443 \
# -v $(pwd)/build:/usr/share/nginx/html:ro \
# --rm \
# nginx
+245 -36
View File
@@ -3,7 +3,9 @@ import { useInterval } from 'react-powerhooks';
import uuid from "uuid";
import {Link} from 'react-router-dom';
import TextField from '@material-ui/core/TextField';
import Drawer from '@material-ui/core/Drawer';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
@@ -23,7 +25,12 @@ import Input from '@material-ui/core/Input';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
import CircularProgress from '@material-ui/core/CircularProgress';
import ReactJson from 'react-json-view'
import DirectionsRunIcon from '@material-ui/icons/DirectionsRun';
import PolymerIcon from '@material-ui/icons/Polymer';
import CreateIcon from '@material-ui/icons/Create';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import AspectRatioIcon from '@material-ui/icons/AspectRatio';
@@ -111,6 +118,7 @@ const AngularWorkflow = (props) => {
const [workflowDone, setWorkflowDone] = React.useState(false)
const [localFirstrequest, setLocalFirstrequest] = React.useState(true)
const [requiresAuthentication, setRequiresAuthentication] = React.useState(true)
const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false)
const [variableAnchorEl, setVariableAnchorEl] = React.useState(null)
@@ -148,6 +156,9 @@ const AngularWorkflow = (props) => {
const [, setExecutingNodes] = React.useState([])
const [executionRunning, setExecutionRunning] = React.useState(false)
const [executionModalOpen, setExecutionModalOpen] = React.useState(false)
const [executionModalView, setExecutionModalView] = React.useState(0)
const [executionData, setExecutionData] = React.useState({})
const [lastSaved, setLastSaved] = React.useState(true)
@@ -269,6 +280,14 @@ const AngularWorkflow = (props) => {
//console.log(responseJson)
// Loop nodes and find results
// Update on every interval? idk
if (JSON.stringify(responseJson) !== JSON.stringify(executionData)) {
// FIXME: If another is selected, don't edit..
// Doesn't work because this is some async garbage
if (executionData.execution_id === undefined || responseJson.execution_id === executionData.execution_id) {
setExecutionData(responseJson)
}
}
if (responseJson.execution_id !== executionRequest.execution_id) {
cy.elements().removeClass('success-highlight failure-highlight executing-highlight')
return
@@ -326,7 +345,8 @@ const AngularWorkflow = (props) => {
if (!visited.includes(item.action.label)) {
if (executionRunning) {
alert.show("Success for "+item.action.label+" with result "+item.result)
alert.show("Success in node "+item.action.label)
//+" with result "+item.result)
visited.push(item.action.label)
setVisited(visited)
}
@@ -640,6 +660,9 @@ const AngularWorkflow = (props) => {
"authorization": responseJson.authorization,
})
setExecutingNodes([workflow.start])
setExecutionData({})
setExecutionModalOpen(true)
setExecutionModalView(1)
start()
})
.catch(error => {
@@ -792,7 +815,8 @@ const AngularWorkflow = (props) => {
//setSelectedTriggerIndex(-1)
//setTriggerFolders([])
//setLocalFirstrequest(true)
// Can be used for right side view
setRightSideBarOpen(false)
console.timeEnd("UNSELECT")
}
@@ -1440,8 +1464,20 @@ const AngularWorkflow = (props) => {
}
const handleVariablesHoverOut = () => {
setVariablesHoverColor(hoverOutColor)
}
setVariablesHoverColor(hoverOutColor)
}
const paperVariableStyle = {
minHeight: "50px",
maxHeight: "50px",
minWidth: "100%",
maxWidth: "100%",
marginTop: "5px",
color: "white",
backgroundColor: surfaceColor,
cursor: "pointer",
display: "flex",
}
const VariablesView = () => {
const [open, setOpen] = React.useState(false);
@@ -1461,19 +1497,7 @@ const AngularWorkflow = (props) => {
</div>
</div>
)
}
const paperVariableStyle = {
minHeight: "50px",
maxHeight: "50px",
minWidth: "100%",
maxWidth: "100%",
marginTop: "5px",
color: "white",
backgroundColor: surfaceColor,
cursor: "pointer",
display: "flex",
}
}
const menuClick = (event) => {
setOpen(!open)
@@ -2520,10 +2544,10 @@ const AngularWorkflow = (props) => {
const appApiView = Object.getOwnPropertyNames(selectedAction).length > 0 && Object.getOwnPropertyNames(selectedApp).length > 0 ?
<div style={appApiViewStyle}>
<div style={{display: "flex", height: "40px", marginBottom: 30}}>
<div style={{display: "flex", height: 40, marginBottom: 30}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}}>{selectedAction.app_name}</h3>
<a href="/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}}>What are apps?</a>
<Link to="/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}}>What are apps?</Link>
</div>
<div style={{flex: "1"}}>
<Button disabled={selectedAction.id === workflow.start} style={{zIndex: 5000, marginTop: "15px",}} color="primary" variant="outlined" onClick={(e) => {
@@ -2635,7 +2659,7 @@ const AngularWorkflow = (props) => {
: null
const headerSize = 74
const headerSize = 68
const rightsidebarStyle = {
position: "fixed",
right: 0,
@@ -3248,7 +3272,7 @@ const AngularWorkflow = (props) => {
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}} >Branch: Conditions - {selectedEdgeIndex}</h3>
<a href="/docs/conditions" style={{textDecoration: "none", color: "#f85a3e"}}>What are conditions?</a>
<Link to="/docs/conditions" style={{textDecoration: "none", color: "#f85a3e"}}>What are conditions?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
@@ -3478,7 +3502,7 @@ const AngularWorkflow = (props) => {
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
<a href="/docs/webhooks" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</a>
<Link to="/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
@@ -3564,7 +3588,7 @@ const AngularWorkflow = (props) => {
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
<a href="/docs/webhooks" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</a>
<Link to="/docs/triggers#webhook" style={{textDecoration: "none", color: "#f85a3e"}}>What are webhooks?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
@@ -3914,7 +3938,7 @@ const AngularWorkflow = (props) => {
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
<a href="/docs/schedules" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</a>
<Link to="/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
@@ -4043,7 +4067,7 @@ const AngularWorkflow = (props) => {
<div style={{display: "flex", height: "40px", marginBottom: "30px"}}>
<div style={{flex: "1"}}>
<h3 style={{marginBottom: "5px"}}>{selectedTrigger.app_name}: {selectedTrigger.status}</h3>
<a href="/docs/schedules" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</a>
<Link to="/docs/triggers#schedule" style={{textDecoration: "none", color: "#f85a3e"}}>What are schedules?</Link>
</div>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
@@ -4167,7 +4191,7 @@ const AngularWorkflow = (props) => {
return null
}
const cytoscapeViewWidths = 600
const cytoscapeViewWidths = 650
const bottomBarStyle = {
position: "fixed",
right: 20,
@@ -4195,7 +4219,17 @@ const AngularWorkflow = (props) => {
return (
<div style={topBarStyle}>
<div style={{margin: 10}}>
<h3>Editing workflow {workflow.name}</h3>
<Breadcrumbs aria-label="breadcrumb" separator="" style={{color: "white",}}>
<Link to="/workflows" style={{textDecoration: "none", color: "inherit",}}>
<h2 style={{color: "rgba(255,255,255,0.5)"}}>
<PolymerIcon style={{marginRight: 10}} />
Workflows
</h2>
</Link>
<h2>
{workflow.name}
</h2>
</Breadcrumbs>
</div>
</div>
)
@@ -4219,7 +4253,7 @@ const AngularWorkflow = (props) => {
const BottomCytoscapeBar = () => {
const boxSize = 100
const executionButton = executionRunning ?
<Tooltip color="primary" title="Stop running workflow (ctrl+a)" placement="top">
<Tooltip color="primary" title="Stop execution" placement="top">
<Button style={{height: boxSize, width: boxSize}} color="secondary" variant="contained" onClick={() => {
abortExecution()
}}>
@@ -4227,7 +4261,7 @@ const AngularWorkflow = (props) => {
</Button>
</Tooltip>
:
<Tooltip color="primary" title="Execute workflow once (ctrl+a)" placement="top">
<Tooltip color="primary" title="Test execution" placement="top">
<Button disabled={executionRequestStarted} style={{height: boxSize, width: boxSize}} color="primary" variant="contained" onClick={() => {
executeWorkflow()
}}>
@@ -4244,8 +4278,11 @@ const AngularWorkflow = (props) => {
style={{backgroundColor: inputColor, }}
InputProps={{
style:{
color: "white",
height: 50,
color: "white",
marginLeft: 5,
maxWidth: "95%",
fontSize: "1em",
},
}}
color="secondary"
@@ -4273,6 +4310,13 @@ const AngularWorkflow = (props) => {
<DeleteIcon />
</Button>
</Tooltip>
<Tooltip color="secondary" title="Show executions" placement="top-start">
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
setExecutionModalOpen(true)
}}>
<DirectionsRunIcon />
</Button>
</Tooltip>
</div>
</div>
)
@@ -4281,6 +4325,7 @@ const AngularWorkflow = (props) => {
const RightSideBar = () => {
setLastSaved(false)
if (Object.getOwnPropertyNames(selectedAction).length > 0 && Object.getOwnPropertyNames(selectedApp).length > 0) {
setRightSideBarOpen(true)
//console.time('ACTIONSTART');
return(
<div style={rightsidebarStyle}>
@@ -4289,6 +4334,7 @@ const AngularWorkflow = (props) => {
)
} else if (Object.getOwnPropertyNames(selectedTrigger).length > 0) {
if (selectedTrigger.trigger_type === "SCHEDULE") {
setRightSideBarOpen(true)
console.log("SCHEDULE")
return(
<div style={rightsidebarStyle}>
@@ -4296,6 +4342,7 @@ const AngularWorkflow = (props) => {
</div>
)
} else if (selectedTrigger.trigger_type === "WEBHOOK") {
setRightSideBarOpen(true)
console.log("WEBHOOK")
return(
<div style={rightsidebarStyle}>
@@ -4303,6 +4350,7 @@ const AngularWorkflow = (props) => {
</div>
)
} else if (selectedTrigger.trigger_type === "EMAIL") {
setRightSideBarOpen(true)
console.log("EMAIL")
return(
<div style={rightsidebarStyle}>
@@ -4310,6 +4358,7 @@ const AngularWorkflow = (props) => {
</div>
)
} else if (selectedTrigger.trigger_type === "USERINPUT") {
setRightSideBarOpen(true)
console.log("USER INPUT SIDEBAR")
return(
<div style={rightsidebarStyle}>
@@ -4323,6 +4372,7 @@ const AngularWorkflow = (props) => {
return null
}
} else if (Object.getOwnPropertyNames(selectedEdge).length > 0) {
setRightSideBarOpen(true)
return(
<div style={rightsidebarStyle}>
<EdgeSidebar />
@@ -4356,6 +4406,165 @@ const AngularWorkflow = (props) => {
</div>
</div>
const executionPaperStyle = {
minWidth: "95%",
maxWidth: "95%",
marginTop: "5px",
color: "white",
marginBottom: 10,
padding: 5,
backgroundColor: surfaceColor,
cursor: "pointer",
display: "flex",
minHeight: 40,
maxHeight: 40,
}
const executionModal =
<Drawer anchor={"right"} open={executionModalOpen} onClose={() => setExecutionModalOpen(false)} PaperProps={{style: {minWidth: 375, maxWidth: 375, backgroundColor: "#1F2023", color: "white", fontSize: 18}}}>
{executionModalView === 0 ?
<div style={{padding: 25, }}>
<Breadcrumbs aria-label="breadcrumb" separator="" style={{color: "white", fontSize: 16}}>
<h2 style={{color: "rgba(255,255,255,0.5)"}}>
<DirectionsRunIcon style={{marginRight: 10}} />
All Executions
</h2>
</Breadcrumbs>
<Button
style={{borderRadius: "0px"}}
onClick={() => {
getWorkflowExecution(props.match.params.key)
}} color="primary">
Refresh executions
</Button>
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
{workflowExecutions.length > 0 ?
<div>
{workflowExecutions.map(data => {
const statusColor = data.status === "FINISHED" ? "green" : data.status === "ABORTED" ? "red" : "orange"
const timeElapsed = data.completed_at-data.started_at
const resultsLength = data.results !== undefined && data.results !== null ? data.results.length : 0
const timestamp = new Date(data.started_at*1000).toISOString().split('.')[0].split("T").join(" ")
return (
<Paper elevation={5} square style={executionPaperStyle} onMouseOver={() => {}} onMouseOut={() => {}} onClick={() => {
setExecutionModalView(1)
setExecutionData(data)
}}>
<div style={{display: "flex", flex: 1}}>
<div style={{marginLeft: 5, width: 2, backgroundColor: statusColor, marginRight: 15}} />
<div style={{ marginTop: "auto", marginBottom: "auto", marginRight: 15, }}>
{timestamp}
</div>
<Tooltip color="primary" title={resultsLength+" actions ran"} placement="top">
<div style={{marginRight: 10, marginTop: "auto", marginBottom: "auto",}}>
{resultsLength}/{data.workflow.actions.length}
</div>
</Tooltip>
</div>
<Tooltip title={"Inspect execution"} placement="top">
<KeyboardArrowRightIcon style={{marginTop: "auto", marginBottom: "auto"}}/>
</Tooltip>
</Paper>
)
return
})}
</div>
:
<div>
There are no executions yet
</div>
}
</div>
:
<div style={{padding: 25, }}>
<Breadcrumbs aria-label="breadcrumb" separator="" style={{color: "white", fontSize: 16}}>
<h2 style={{color: "rgba(255,255,255,0.5)", cursor: "pointer"}} onClick={() => {setExecutionModalView(0)}}>
<DirectionsRunIcon style={{marginRight: 10}} />
Other Executions
</h2>
</Breadcrumbs>
<Divider style={{backgroundColor: "white", marginTop: 10, marginBottom: 10,}}/>
<h2>Executing Workflow</h2>
{executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ?
<div>
<h3>Execution Argument: </h3>{executionText}
</div>
: null }
{executionData.status !== undefined && executionData.status.length > 0 ?
<div>
<b>Status: </b>{executionData.status}
</div>
: null
}
{executionData.started_at !== undefined ?
<div>
<b>Started: </b>{new Date(executionData.started_at*1000).toISOString()}
</div>
: null
}
{executionData.execution_id !== undefined && executionData.execution_id.length > 0 ?
<div>
<b>ID: </b>{executionData.execution_id}
</div>
: null
}
<Divider style={{backgroundColor: "white", marginTop: 30, marginBottom: 30,}}/>
<div style={{display: "flex", marginTop: 10, marginBottom: 30,}}>
<b>Actions</b>
<div>
{executionData.status !== undefined && executionData.status !== "ABORTED" && executionData.status !== "FINISHED" ? <CircularProgress style={{marginLeft: 20}}/> : null}
</div>
</div>
{executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING" ?
<CircularProgress />
:
executionData.results.map(data => {
var showResult = data.result.trim()
showResult.split(" None").join(" \"None\"")
//showResult = replaceAll(showResult, " None", " \"None\"");
var jsonvalid = true
try {
JSON.parse(showResult)
} catch (e) {
jsonvalid = false
}
const curapp = apps.find(a => a.name === data.action.app_name && a.app_version === data.action.app_version)
const imgsize = 50
const actionimg = curapp === null ?
null :
<img alt={data.action.app_name} src={curapp.large_image} style={{marginRight: 20, width: imgsize, height: imgsize}} />
return (
<div style={{marginBottom: 40,}}>
<div style={{display: "flex", marginBottom: 15,}}>
{actionimg}
<span style={{fontSize: 24, marginTop: "auto", marginBottom: "auto"}}><b>{data.action.label}</b></span>
</div>
<div style={{marginBottom: 5}}><b>Status </b> {data.status}</div>
{jsonvalid ? <ReactJson
src={JSON.parse(showResult)}
theme="solarized"
collapsed={false}
displayDataTypes={false}
name={"Results for "+data.action.label}
/>
:
<div>
<b>Result</b>&nbsp;
{data.result}
</div>
}
</div>
)
})
}
</div>
}
</Drawer>
const newView = isLoggedIn ?
<div style={{color: "white"}}>
<div style={{display: "flex", borderTop: "1px solid rgba(91, 96, 100, 1)"}}>
@@ -4373,17 +4582,17 @@ const AngularWorkflow = (props) => {
}}
/>
</div>
{executionModal}
<RightSideBar />
<BottomCytoscapeBar />
<TopCytoscapeBar />
<NoActionsBar />
{debugView}
</div>
:
<div style={{color: "white"}}>
TMP FOR NOT LOGGED IN
</div>
const variablesModal = variablesModalOpen ?
<Dialog modal
open={variablesModalOpen}
@@ -4446,7 +4655,7 @@ const AngularWorkflow = (props) => {
/>
</DialogContent>
<DialogActions>
<Button
<Button
style={{borderRadius: "0px"}}
onClick={() => {
setNewVariableName("")
@@ -4456,7 +4665,7 @@ const AngularWorkflow = (props) => {
}} color="primary">
Cancel
</Button>
<Button style={{borderRadius: "0px"}} disabled={newVariableName.length === 0 || newVariableValue.length === 0} onClick={() => {
<Button style={{borderRadius: "0px"}} disabled={newVariableName.length === 0 || newVariableValue.length === 0} onClick={() => {
if (workflow.workflow_variables === undefined || workflow.workflow_variables === null) {
workflow.workflow_variables = []
}
@@ -4598,14 +4807,14 @@ const AngularWorkflow = (props) => {
>
<DialogTitle><div style={{color: "white"}}>Authentication for {selectedApp.name}</div></DialogTitle>
<DialogContent>
<a href="/docs/apps#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</a>
<Link to="/docs/apps#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</Link>
<div />
{selectedApp.link.length > 0 ? <EndpointData /> : null}
<div style={{marginTop: 15, marginBottom: 15, }}/>
<AuthenticationData />
</DialogContent>
<DialogActions>
<Button
<Button
style={{borderRadius: "0px"}}
onClick={() => {
setAuthenticationModalOpen(false)
+7 -7
View File
@@ -37,13 +37,13 @@ import { createMuiTheme } from '@material-ui/core/styles';
import AlertTemplate from "react-alert-template-basic";
import { positions, Provider } from "react-alert";
// Testing - localhost
//const globalUrl = "http://192.168.3.6:5001"
//console.log("HOST: ", process.env)
// Production - backend proxy forwarding in nginx
const globalUrl = window.location.origin
var globalUrl = window.location.origin
if (window.location.protocol == "http:" && window.location.port === "3000") {
globalUrl = "http://192.168.3.6:5001"
}
console.log(window.location)
console.log(globalUrl)
const surfaceColor = "#27292D"
const inputColor = "#383B40"
@@ -79,7 +79,7 @@ const App = (message, props) => {
}})
if (isLoaded && !isLoggedIn && (!window.location.pathname.startsWith("/login") && (!window.location.pathname.startsWith("/docs") && (!window.location.pathname.startsWith("/adminsetup"))))) {
window.location = "login"
window.location = "/login"
}
const checkLogin = () => {
+22 -10
View File
@@ -81,7 +81,7 @@ const AppCreator = (props) => {
const [actionsModalOpen, setActionsModalOpen] = useState(false);
const [authenticationOption, setAuthenticationOption] = useState(authenticationOptions[0]);
const [parameterName, setParameterName] = useState("");
const [parameterLocation, setParameterLocation] = useState(apikeySelection[0]);
const [parameterLocation, setParameterLocation] = useState(apikeySelection.length > 0 ? apikeySelection[0] : "");
const [urlPath, setUrlPath] = useState("");
//const [urlPathQueries, setUrlPathQueries] = useState([{"name": "test", "required": false}]);
const [urlPathQueries, setUrlPathQueries] = useState([]);
@@ -161,7 +161,7 @@ const AppCreator = (props) => {
.then((responseJson) => {
setIsAppLoaded(true)
if (!responseJson.success) {
alert.error("Failed to verify")
alert.error("Failed to get the app")
} else {
const data = JSON.parse(responseJson.body)
console.log("LOADED IMAGE: ", data.image)
@@ -263,9 +263,12 @@ const AppCreator = (props) => {
break
} else if (value.type === "apiKey") {
setAuthenticationOption("API key")
setParameterName(value.name)
setParameterName(value.name)
value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1);
setParameterLocation(value.in)
if (!apikeySelection.includes(value.in)) {
console.log("APIKEY SELECT: ", apikeySelection)
alert.error("Might be error in setting up API key authentication")
}
break
@@ -593,6 +596,9 @@ const AppCreator = (props) => {
setActions(actions)
}
console.log("Option: ", authenticationOption)
console.log("Location: ", parameterLocation)
console.log("Name: ", parameterName)
const apiKey = authenticationOption === "API key" ?
<div>
<h4>API key</h4>
@@ -605,6 +611,7 @@ const AppCreator = (props) => {
id="standard-required"
margin="normal"
variant="outlined"
defaultValue={parameterName}
helperText={<div style={{color:"white", marginBottom: "2px",}}>Can't be empty. Can't contain any of the following characters: !#$%&'^+-._~|]+$</div>}
onChange={e => setParameterName(e.target.value)}
InputProps={{
@@ -628,13 +635,18 @@ const AppCreator = (props) => {
name: 'age',
id: 'outlined-age-simple',
}}
>
>
{apikeySelection.map(data => (
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data}>
{data}
</MenuItem>
))}
>
{apikeySelection.map(data => {
if (data === undefined) {
return null
}
return (
<MenuItem style={{backgroundColor: inputColor, color: "white"}} value={data}>
{data}
</MenuItem>
)}
)}
</Select>
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
</div>
+59 -35
View File
@@ -12,6 +12,8 @@ import TextField from '@material-ui/core/TextField';
import FormControl from '@material-ui/core/FormControl';
import MenuItem from '@material-ui/core/MenuItem';
import Tooltip from '@material-ui/core/Tooltip';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Switch from '@material-ui/core/Switch';
import Input from '@material-ui/core/Input';
import YAML from 'yaml'
import {Link} from 'react-router-dom';
@@ -43,6 +45,7 @@ const Apps = (props) => {
const [isLoading, setIsLoading] = React.useState(false)
const [appSearchLoading, setAppSearchLoading] = React.useState(false)
const [selectedAction, setSelectedAction] = React.useState({})
const [searchBackend, setSearchBackend] = React.useState(false)
const [openApi, setOpenApi] = React.useState("")
const [openApiData, setOpenApiData] = React.useState("")
@@ -186,7 +189,7 @@ const Apps = (props) => {
}
var imageline = data.large_image.length === 0 ?
<img alt="Image missing" style={{width: 100, height: 100}} />
<img alt={data.title} style={{width: 100, height: 100}} />
:
<img alt={data.title} src={data.large_image} style={{width: 100, height: 100, maxWidth: "100%"}} />
@@ -262,7 +265,7 @@ const Apps = (props) => {
const dividerColor = "rgb(225, 228, 232)"
const uploadViewPaperStyle = {
minWidth: "100%",
maxWidth: "100%",
maxWidth: 662.5,
color: "white",
backgroundColor: surfaceColor,
display: "flex",
@@ -285,9 +288,10 @@ const Apps = (props) => {
var description = selectedApp.description
const url = "/apps/edit/"+selectedApp.id
var editButton = selectedApp.private_id !== undefined && selectedApp.private_id.length > 0 && selectedApp.generated ?
<Link to={url} style={{textDecoration: "none"}}>
const editUrl = "/apps/edit/"+selectedApp.id
const activateUrl = "/apps/new?id="+selectedApp.id
var editButton = selectedApp.activated && selectedApp.private_id !== undefined && selectedApp.private_id.length > 0 && selectedApp.generated ?
<Link to={editUrl} style={{textDecoration: "none"}}>
<Button
variant="outlined"
component="label"
@@ -297,8 +301,18 @@ const Apps = (props) => {
Edit app
</Button></Link> : null
var activateButton = selectedApp.generated && !selectedApp.activated ?
<Link to={activateUrl} style={{textDecoration: "none"}}>
<Button
variant="contained"
component="label"
color="primary"
style={{marginTop: "10px"}}
>
Activate App
</Button></Link> : null
var deleteButton = (selectedApp.private_id !== undefined && selectedApp.private_id.length > 0 && selectedApp.generated) || (selectedApp.downloaded != undefined && selectedApp.downloaded == true) ?
var deleteButton = ((selectedApp.private_id !== undefined && selectedApp.private_id.length > 0 && selectedApp.generated) || (selectedApp.downloaded != undefined && selectedApp.downloaded == true)) && activateButton === null ?
<Button
variant="outlined"
component="label"
@@ -311,16 +325,30 @@ const Apps = (props) => {
Delete app
</Button> : null
var imageline = selectedApp.large_image === undefined || selectedApp.large_image.length === 0 ?
<img alt={selectedApp.title} style={{width: 100, height: 100}} />
:
<img alt={selectedApp.title} src={selectedApp.large_image} style={{width: 100, height: 100, maxWidth: "100%"}} />
//fetch(globalUrl+"/api/v1/get_openapi/"+urlParams.get("id"), {
var baseInfo = newAppname.length > 0 ?
<div>
<h2>{newAppname}</h2>
<p>{description}</p>
<div style={{display: "flex"}}>
<div style={{marginRight: 15, marginTop: 10}}>
{imageline}
</div>
<div style={{maxWidth: "75%", overflow: "hidden"}}>
<h2>{newAppname}</h2>
<p>{description}</p>
</div>
</div>
{activateButton}
{editButton}
{deleteButton}
<Divider style={{marginBottom: "10px", marginTop: "10px", backgroundColor: dividerColor}}/>
{selectedApp.link.length > 0 ? <p>URL: {selectedApp.link}</p> : null}
<p>ID: {selectedApp.id}</p>
{selectedApp.privateId !== undefined && selectedApp.privateId.length > 0 ? <p>PrivateID: {selectedApp.privateId}</p> : null}
{selectedApp.link.length > 0 ? <p><b>URL:</b> {selectedApp.link}</p> : null}
<p><b>ID:</b> {selectedApp.id}</p>
{selectedApp.privateId !== undefined && selectedApp.privateId.length > 0 ? <p><b>PrivateID:</b> {selectedApp.privateId}</p> : null}
<div style={{marginTop: 15, marginBottom: 15}}>
<b>Actions</b>
@@ -356,7 +384,7 @@ const Apps = (props) => {
</Select>
</div>
{selectedAction.parameters !== undefined ?
{selectedAction.parameters !== undefined && selectedAction.parameters !== null ?
<div style={{marginTop: 15, marginBottom: 15}}>
<b>Arguments</b>
{selectedAction.parameters.map(data => {
@@ -376,19 +404,17 @@ const Apps = (props) => {
})}
</div>
: null}
{editButton}
{deleteButton}
</div>
:
null
return(
<div>
<div style={{}}>
<Paper square style={uploadViewPaperStyle}>
<div style={{width: "100%", margin: 25}}>
<h2>App Creator</h2>
<a href="https://github.com/frikky/OpenAPI-security-definitions" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">Security API's</a>
<a href="/docs/apps" style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">How it works</a>
&nbsp;- <a href="https://github.com/frikky/OpenAPI-security-definitions" 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>
<div/>
Apps interact with eachother in workflows. They are created with the app creator, using OpenAPI specification or manually in python. Use the links above to find potential apps you're looking for using OpenAPI or make one from scratch. There's 1000+ available.
@@ -431,7 +457,8 @@ const Apps = (props) => {
const searchfield = event.target.value.toLowerCase()
const newapps = apps.filter(data => data.name.toLowerCase().includes(searchfield) || data.description.toLowerCase().includes(searchfield))
if (newapps.length === 0 && !appSearchLoading) {
if ((newapps.length === 0 || searchBackend) && !appSearchLoading) {
setAppSearchLoading(true)
runAppSearch(searchfield)
} else {
@@ -445,7 +472,7 @@ const Apps = (props) => {
<div style={{flex: "1", marginLeft: 10, marginRight: 10}}>
<h2>Upload</h2>
<div style={{marginTop: 20}}/>
<UploadView />
<UploadView/>
</div>
<Divider style={{marginBottom: "10px", marginTop: "10px", height: "100%", width: "1px", backgroundColor: dividerColor}}/>
<div style={{flex: 1, marginLeft: 10, marginRight: 10}}>
@@ -454,19 +481,11 @@ const Apps = (props) => {
<h2>Available integrations</h2>
</div>
{isLoading ? <CircularProgress style={{marginTop: 13, marginRight: 15}} /> : null}
{/*
<Button
variant="outlined"
component="label"
color="primary"
style={{margin: 5, maxHeight: 50, marginTop: 10}}
onClick={() => {
getSpecificApps(baseRepository)
}}
>
Load apps
</Button>
*/}
<FormControlLabel
style={{color: "white", marginBottom: "0px", marginTop: "10px"}}
label=<div style={{color: "white"}}>Search OpenAPI</div>
control={<Switch checked={searchBackend} onChange={() => {setSearchBackend(!searchBackend)}} />}
/>
<Button
variant="outlined"
component="label"
@@ -477,7 +496,7 @@ const Apps = (props) => {
setLoadAppsModalOpen(true)
}}
>
Download from URL
Download more apps
</Button>
</div>
<TextField
@@ -511,7 +530,7 @@ const Apps = (props) => {
:
<Paper square style={uploadViewPaperStyle}>
<h4>
Try a broader search term. E.g. "http" or "TheHive"
Try a broader search term, e.g. "http", "alert", "ticket" etc.
</h4>
<div/>
@@ -651,7 +670,12 @@ const Apps = (props) => {
return response.json()
})
.then((responseJson) => {
console.log(responseJson)
//console.log(responseJson)
if (responseJson.success) {
if (responseJson.reason !== undefined && responseJson.reason.length > 0) {
setFilteredApps(responseJson.reason)
}
}
})
.catch(error => {
alert.error(error.toString())
+5 -5
View File
@@ -32,7 +32,7 @@ import DialogContent from '@material-ui/core/DialogContent';
const surfaceColor = "#27292D"
const Workflows = (props) => {
const { globalUrl, isLoggedIn, isLoaded, } = props;
const { globalUrl, isLoggedIn, isLoaded, } = props;
document.title = "Shuffle - Workflows"
const alert = useAlert()
@@ -158,7 +158,7 @@ const Workflows = (props) => {
const getWorkflowExecution = (id) => {
fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", {
method: 'GET',
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
@@ -186,7 +186,7 @@ const Workflows = (props) => {
const abortExecution = (workflowid, executionid) => {
alert.success("Aborting execution")
fetch(globalUrl+"/api/v1/workflows/"+workflowid+"/executions/"+executionid+"/abort", {
method: 'GET',
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
@@ -210,7 +210,7 @@ const Workflows = (props) => {
alert.show("Executing workflow "+id)
setTrackingId(id)
fetch(globalUrl+"/api/v1/workflows/"+id+"/execute", {
method: 'GET',
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
@@ -290,7 +290,7 @@ const Workflows = (props) => {
const deleteWorkflow = (id) => {
alert.success("Deleted workflow "+id)
fetch(globalUrl+"/api/v1/workflows/"+id, {
method: 'DELETE',
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
+3
View File
@@ -17,3 +17,6 @@ The point of this folder is to make GCP Cloud functions able to run default WALK
* class AppBase
* class <APP>
* Runner
## Update may 2020:
Moved static_baseline to ./onprem/app_sdk because of license.
+9 -3
View File
@@ -1,4 +1,10 @@
docker rmi frikky/shuffle:orborus --force
NAME=orborus
VERSION=0.1.0
docker build . -t frikky/shuffle:orborus
docker push frikky/shuffle:orborus
echo "Running docker build with $NAME:$VERSION"
#docker rmi frikky/shuffle:$NAME --force
docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t frikky/$NAME:$VERSION
docker push frikky/$NAME:$VERSION
docker push frikky/shuffle:$NAME
#docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION
+8 -1
View File
@@ -146,6 +146,9 @@ func initializeImages(dockercli *dockerclient.Client) {
ctx := context.Background()
// check whether theyre the same first
//version := "0.1.0"
// fmt.Sprintf("docker.pkg.github.com/frikky/shuffle/orborus:%s", version),
// fmt.Sprintf("docker.pkg.github.com/frikky/shuffle/worker:%s", version),
images := []string{
fmt.Sprintf("docker.io/%s:app_sdk", baseimagename),
fmt.Sprintf("docker.io/%s:worker", baseimagename),
@@ -155,7 +158,7 @@ func initializeImages(dockercli *dockerclient.Client) {
for _, image := range images {
reader, err := dockercli.ImagePull(ctx, image, pullOptions)
if err != nil {
log.Printf("Failed getting %s", image)
log.Printf("Failed getting %s: %s", image, err)
continue
}
@@ -197,6 +200,10 @@ func main() {
log.Printf("--- Setting up Docker environment. Downloading worker and App SDK! ---")
initializeImages(dockercli)
//workerName := "worker"
//workerVersion := "0.1.0"
//workerImage := fmt.Sprintf("docker.pkg.github.com/frikky/shuffle/%s:%s", workerName, workerVersion)
workerImage := fmt.Sprintf("%s:worker", baseimagename)
log.Printf("--- Finished configuring docker environment ---\n")
+11
View File
@@ -0,0 +1,11 @@
NAME=worker
VERSION=0.1.0
echo "Running docker build with $NAME:$VERSION"
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t frikky/$NAME:$VERSION
# Push both for now..
docker push frikky/$NAME:$VERSION
docker push frikky/shuffle:$NAME
#docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION
+11 -11
View File
@@ -1,15 +1,15 @@
echo "Compiling program"
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
echo "Fixing docker env"
docker rmi frikky/shuffle:worker --force
docker build . -t frikky/shuffle:worker
docker push frikky/shuffle:worker
#docker rmi frikky/shuffle:worker --force
#docker build . -t frikky/shuffle:worker
#docker push frikky/shuffle:worker
#docker run \
# --env "AUTHORIZATION=ASD" \
# --env "DOCKER_API_VERSION=1.39" \
# --env "EXECUTIONID=ASD" \
# --env "BASE_URI=$BASE_URI" \
# -v /var/run/docker.sock:/var/run/docker.sock \
# frikky/shuffle:worker
docker run \
--env "AUTHORIZATION=ASD" \
--env "DOCKER_API_VERSION=1.39" \
--env "EXECUTIONID=ASD" \
--env "BASE_URI=$BASE_URI" \
-v /var/run/docker.sock:/var/run/docker.sock \
frikky/shuffle:worker
+1
View File
@@ -168,6 +168,7 @@ func sendRequest(token OauthToken, message TeamsHook) error {
return nil
}
// If you're finding this: its from a test project :)
func get_accesstoken() (OauthToken, error) {
client_id := "9a2a2a63-c63c-4487-baf0-4ff3f4873a7f"
client_secret := ":3]D6oFimiXbuV20xH?Dzu@LR*6IFVbq"
-57
View File
@@ -1,57 +0,0 @@
# Outlook trigger
Makes it possible to trigger a workflow based on an email
## Local testing - Same as ../webhook
```bash
mv ../main.go
go run main.go hook.go
```
# Deploy gcloud
gcloud functions deploy outlooktrigger --runtime go111 --entry-point Authorization --trigger-http --project shuffler --memory=128 --set-env-vars=FUNCTION_APIKEY=asdasd,CALLBACKURL=shuffler.io,TRIGGERID=test123,WORKFLOW_ID=YOUR_WORKFLOW_ID
# Build and deploy
1. Set hook.go line 1 from "package main" to "package function"
2. zip outlooktrigger.tar hook.go
3. Upload to bucket https://console.cloud.google.com/storage/browser/shuffler.appspot.com?project=shuffler
4. Go to the functions https://console.cloud.google.com/functions/list?project=shuffler
## How it works (from frontend to backend)
### Choose mailfolders
1. Use microsoft graph api to get the folders the user wants to listen to
* Have the user write their primary email (default) or another one
* Have it show the folders for the email with chooseable buttons somehow
|inbox
|-subinbox
|--subsubinbox <-- choose e.g. this one
|otherfolder
API:
// requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/me/mailfolders")
### Add callback subscription
2. Make an APIcall to ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages") with callback url defined as "https://shuffler.io/api/v1/workflows/{key}/email/authorize"
* Should this be set up whenever the user clicked start or when the workflow is created?
* Start click ->
1. Add another cloud function for the item
2. When it's ready, deploy it to authorize
3. Show it as ready
### Remove a subscription
* Since everything is already generated above, one would need to
* https://docs.microsoft.com/en-us/graph/api/subscription-delete?view=graph-rest-1.0&tabs=http
* DELETE https://graph.microsoft.com/v1.0/subscriptions/{id}
## CREATE - Fixme: LIST all current subscriptions, and stop them if they're towards the same endpoint
* POST /api/v1/workflows/{key}/outlook
* createOutlookSub(resp, request)
* getOutlookSubscriptions(client) // Used to remove all existing for same endpoint
* makeOutlookSubscription(client, folderIds, notificationUrl)
* Add data from ^ to triggerAuth
## DELETE
* DELETE /api/v1/workflows/{key}/outlook/{triggerId}
* handleDeleteOutlookSub(resp, request)
* handleOutlookSubRemoval(workflowId, triggerId)
-222
View File
@@ -1,222 +0,0 @@
package function
// Shuffle:
// https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Authentication/appId/e080cbf4-5dba-44b4-8643-a7c982189c16/isMSAApp//defaultBlade/Overview/servicePrincipalCreated/true
// Oauth playground:
// https://oauthplay.azurewebsites.net/
// APPS:
// https://apps.dev.microsoft.com
// REMOVE ACCESS:
// https://portal.office.com/account/#
// Developer:
// https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference
// Bots:
// https://dev.botframework.com/bots
// Connectors
// https://outlook.office.com/connectors/home/login/#/new
// https://go.microsoft.com/fwlink/?linkid=857599
import (
//"encoding/json"
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
)
type Info struct {
Url string `json:"url" datastore:"url"`
Name string `json:"name" datastore:"name"`
Description string `json:"description" datastore:"description"`
}
// Actions to be done by webhooks etc
// Field is the actual field to use from json
type HookAction struct {
Type string `json:"type" datastore:"type"`
Name string `json:"name" datastore:"name"`
Id string `json:"id" datastore:"id"`
Field string `json:"field" datastore:"field"`
}
type Hook struct {
Id string `json:"id" datastore:"id"`
Info Info `json:"info" datastore:"info"`
Actions []HookAction `json:"actions" datastore:"actions"`
Type string `json:"type" datastore:"type"`
Status string `json:"status" datastore:"status"`
Running bool `json:"running" datastore:"running"`
}
type TeamsHook struct {
MembersAdded []struct {
ID string `json:"id"`
} `json:"membersAdded"`
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
LocalTimestamp string `json:"localTimestamp"`
ID string `json:"id"`
ChannelID string `json:"channelId"`
ServiceURL string `json:"serviceUrl"`
From struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"from"`
Conversation struct {
IsGroup bool `json:"isGroup"`
ConversationType string `json:"conversationType"`
ID string `json:"id"`
TenantID string `json:"tenantId"`
} `json:"conversation"`
Recipient struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"recipient"`
ChannelData struct {
Team struct {
ID string `json:"id"`
} `json:"team"`
EventType string `json:"eventType"`
Tenant struct {
ID string `json:"id"`
} `json:"tenant"`
} `json:"channelData"`
}
var hook Hook
var baseUrl = "https://shuffler.io"
type OauthToken struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
ExtExpiresIn int `json:"ext_expires_in"`
AccessToken string `json:"access_token"`
}
type TeamsResponse struct {
Conversation struct {
ID string `json:"id"`
} `json:"conversation"`
From struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"from"`
Recipient struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"recipient"`
ReplyToId string `json:"replyToId"`
Type string `json:"type"`
Text string `json:"text"`
}
type O365hook struct {
OdataContext string `json:"@odata.context"`
Value []struct {
OdataType string `json:"@odata.type"`
ID interface{} `json:"Id"`
SubscriptionID string `json:"SubscriptionId"`
SubscriptionExpirationDateTime time.Time `json:"SubscriptionExpirationDateTime"`
SequenceNumber int `json:"SequenceNumber"`
ChangeType string `json:"ChangeType"`
Resource string `json:"Resource"`
ResourceData struct {
OdataType string `json:"@odata.type"`
OdataID string `json:"@odata.id"`
OdataEtag string `json:"@odata.etag"`
ID string `json:"Id"`
} `json:"ResourceData"`
} `json:"value"`
}
func Authorization(resp http.ResponseWriter, request *http.Request) {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Body: %s", err)
resp.WriteHeader(403)
return
}
if len(body) > 0 {
// In here - get the email data
// Check who it belongs to and run those workflows
// This should be set from run.go with the callback
// userId: {subscriptionID: {workflowID}}
// E.g. workflow: {auth: {userId
// workflow: {trigger: {
err = forwardRequest(body)
if err != nil {
log.Printf("Failed unmarshal: %s", err)
resp.WriteHeader(403)
return
}
resp.WriteHeader(200)
resp.Write([]byte("OK"))
return
}
token := request.URL.Query().Get("validationToken")
if len(token) == 0 {
log.Println("Validation token is missing")
resp.WriteHeader(403)
return
}
resp.WriteHeader(200)
resp.Write([]byte(string(token)))
}
// GetUserDetails - Get one user's details from randomuser.me API
func forwardRequest(body []byte) error {
callbackUrl := os.Getenv("CALLBACKURL")
workflowId := os.Getenv("WORKFLOW_ID")
apikey := os.Getenv("FUNCTION_APIKEY")
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", callbackUrl, workflowId)
//log.Printf("Sending data to %s", fullUrl)
data := fmt.Sprintf(`{"execution_argument": "%s"}`, string(body))
req, err := http.NewRequest(
http.MethodPost,
fullUrl,
bytes.NewBuffer([]byte(data)),
)
if err != nil {
return err
}
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
randomUserClient := http.Client{
Timeout: time.Second * 5,
}
res, err := randomUserClient.Do(req)
if err != nil {
return err
}
log.Printf("Status: %d", res.StatusCode)
returnbody, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
log.Printf("New body: %s", string(returnbody))
//log.Println(string(newbody))
return nil
}
@@ -1,7 +0,0 @@
{
"clientID": "70e37005-c954-4290-b573-d4b94e484336",
"clientSecret": ".eNw/A[kQFB5zL.agvRputdEJENeJ392",
"RedirectURL": "https://44d84ee7.ngrok.io/functions/outlook/register",
"AuthURL": "https://login.microsoftonline.com/common/oauth2/authorize",
"TokenURL": "https://login.microsoftonline.com/common/oauth2/token"
}
@@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDYDCCAkigAwIBAgIJAOvgxcclM1eyMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAk5PMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgwMzA1MTE1MTIwWhcNMjgwMzAyMTE1MTIwWjBF
MQswCQYDVQQGEwJOTzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAowNqrvocJCLLcytYBfZhsqG3CkPsJ4PTicNyjuUGmlILPHlN1DE7jbDo
KuOKImfV4AfQANnttaPksZyuKJL8XGpC7YmF5mmInUWG48SmZjvRbBi1LnCrjJKS
ywh2lJqw4w30w2ItcpogDrYhh6+T3VyabcYngZjKSFgON3wo4I0c6aT19VVXGqnK
y1WEejZmiChV4iwEu4vMPIzt16QpIBr8NPSkBLLRDAGWMjFnIuYDEwgjVn6XYhM9
+NxhvY9es+qeqLQsZj2a1wcDGaw7G4iNZdltlPmlTCreRDBYBTsYCds/rJmPZ2xi
6jgiyNZj3xHG5Knw2YIw0OwHyT9mWQIDAQABo1MwUTAdBgNVHQ4EFgQUckr5tCzg
F1eBID7mtWTfeqyX4g0wHwYDVR0jBBgwFoAUckr5tCzgF1eBID7mtWTfeqyX4g0w
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAdH1aPcWBjJHF6n/
NgIiRSEU4mi5I6RUlPuR7dN7dcmF1Ho7quurNFzknXwks+a62oKSnkkFxxwrv/d6
dIH5kNibVs7oRxEpA3gUmXXKUW4RPrxAp2zN37t7zs5xbpTATxfJiIMP8Rjo0sOf
SilS22Sn0e0HxBi78t3DJEZvOQ9KSRuD1g9gOAY4lj/fni6rVJo8YCR2MyjmQoXB
luHDF4jTqi/TkXECfqQZu0pctx3maISpB1fAuaELwPDvqbLgoC97gl6bIEiKLkJ0
JkbGnb997K80ztFvFAKGyUtsvEDCupe/fdBPFqCruAQWI/BqVJFTdRD43dWEQANF
S8ApvA==
-----END CERTIFICATE-----
@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAowNqrvocJCLLcytYBfZhsqG3CkPsJ4PTicNyjuUGmlILPHlN
1DE7jbDoKuOKImfV4AfQANnttaPksZyuKJL8XGpC7YmF5mmInUWG48SmZjvRbBi1
LnCrjJKSywh2lJqw4w30w2ItcpogDrYhh6+T3VyabcYngZjKSFgON3wo4I0c6aT1
9VVXGqnKy1WEejZmiChV4iwEu4vMPIzt16QpIBr8NPSkBLLRDAGWMjFnIuYDEwgj
Vn6XYhM9+NxhvY9es+qeqLQsZj2a1wcDGaw7G4iNZdltlPmlTCreRDBYBTsYCds/
rJmPZ2xi6jgiyNZj3xHG5Knw2YIw0OwHyT9mWQIDAQABAoIBABJ+9L/dyQuglwz+
QgKLLhKinq4fftAM+ReMgZcNDW69GGFIMjh9TZCKHg2fu7Cjr3S37jXqhDoz2mL8
sBYSd2fU9rsU+4hlOQb/OIrnaSn4Z46oTwZx6kUM7HL1Bt9dnexlTPxOS3HRYwnI
SI2oslJPi4YhEaJ2v5ztwM8y20B/E/zSW2onWz5gB8/bdxSmuJaWfHioIEoac8Gf
BE7jiYMnx9kKeVfgkKPMBKXhAyE2lbAz7N5nDS/4HkbUMh389RBvakc4gkv/QkSW
LNuXpbcSqJiG0FcVutjYS87a/ul3IdhAYmZDTuvRUhNsWBiY3LSY4C9NlHhiJihg
RU1kknECgYEA1dPT6n4hy/OK2mT3ThGQQFMJWsnmcHSsvuK31+UYZOI9kNtBVxc6
HSHkh0G53o6o2wZLr2gMXy35O5ZxSbectA1Q4MJDlYBs1MfHdOVyE4z6mmA9TLN1
c+9pYOC0qx+6NwPdO7j2xdUMEUfzocWOeay0AzJg20BQYmKuy3Kc+tcCgYEAwyn6
n+XS0vodfJdHhvbW/jocQlHQOBK5HklZfq2PMgpRRDaBOvHP/f07egLc2inec2sC
yPSCEfRMMhFcU5NoBt4Unzz2Y8pbpL1L5kbM6B4IqK/5vYcbvkmBkhL3AFT6Fg/3
3XCdygPW9Vf1nRKr2KhT9dDvB+XO2B75JmKwMk8CgYApKzGf8kz7gZZ4WfwrccI+
QD6K1lihyjUAQ5J15Mv/kHeeDjjUVcqAlWf0irkImpr0IJAt43COWsGjsWF6efmX
yQCLZZuxixppFVXXsd122ivd0S28OMkiWzQEzP67+83Ujc/okcIhcNVz9lB4ExtN
Xe0CuI5haE6RwsI4tYZ33QKBgFq6ckPRcOAZ3IlmPp9Us3/+fdKq/BSFR7/3s347
q11FBKCkghFoBxx5lCPVntxhKIQZlHLdkHZOTvnbrkNAPNUsewPIMHcVxOLiCZ3k
/i9OfxIEtSJR5CjjPTQuUtu5pYWKKN2uE/ytKkpmeM1rt64CGv4lAmp2gGFijMs2
h9jrAoGBANPQO6cKqtnxvst3lnljVBoftlJgeHamUac+xeYKA5Hocv5VwLXMTzzu
09tAhQFvFwCWWrfdgtvIM6k5Sl9F5MdiO9VNflI0IVudIcm9FKorWogH02mtwxsw
hvk5VUk3awiZ/Nu9t38ukeqCetjQEf6yupy/14ZPLndN5naSeEwo
-----END RSA PRIVATE KEY-----
-41
View File
@@ -1,41 +0,0 @@
package main
import (
"log"
"net/http"
"os"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
func webhook() {
// FIXME - remove static
port := ":8080"
baseFilePath := "/"
mux := mux.NewRouter()
mux.SkipClean(true)
// FIXME - Add path for updating the hook? Can be a specific POST requeuest from backend
mux.HandleFunc(baseFilePath, Authorization).Methods("POST")
mux.HandleFunc("/authorize", Authorization).Methods("POST")
handlers.LoggingHandler(os.Stdout, mux)
loggedRouter := handlers.LoggingHandler(os.Stdout, mux)
log.Printf("Starting on http://localhost%s", port)
err := http.ListenAndServe(
port,
loggedRouter,
)
if err != nil {
log.Fatal("ListenAndServer: ", err)
}
}
func main() {
webhook()
}
-304
View File
@@ -1,304 +0,0 @@
package main
// This entire script should be part of the API backend
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"golang.org/x/oauth2"
)
type Subscription struct {
ChangeType string `json:"changeType"`
NotificationURL string `json:"notificationUrl"`
Resource string `json:"resource"`
ExpirationDateTime string `json:"expirationDateTime"`
ClientState string `json:"clientState"`
}
// ClientState string `json:"ClientState,omitempty"`
// OdataType string `json:"@odata.type"`
//odata.type - Include "@odata.type":"#Microsoft.OutlookServices.PushSubscription". The PushSubscription entity defines NotificationURL.
//ChangeType- Specifies the types of events to monitor for that resource. See ChangeType for the supported types.
//ClientState - Optional property that indicates that each notification should be sent with a header by the same ClientState value. This lets the listener check the legitimacy of each notification.
//NotificationURL- Specifies where notifications should be sent to. This URL represents a web service typically implemented by the client.
//Resource- Specifies the resource to monitor and receive notifications on. You can use the optional query parameter $filter to refine the conditions for a notification, or use $select to include specific properties in a rich notification.
//https://outlook.office.com/mail.read
type Config struct {
ClientID string
ClientSecret string
RedirectUrl string
AuthUrl string
TokenUrl string
}
func getOfficeAppInfo() (Config, error) {
configpath := "integrations/config.json"
data, err := ioutil.ReadFile(configpath)
if err != nil {
//log.Fatal(err)
log.Printf("Error getting hive: %s\n", err)
}
config := Config{}
err = json.Unmarshal(data, &config)
if err != nil {
return Config{}, err
}
return config, nil
}
// This should be a popup for the user
func get_accesstoken() (*http.Client, OauthToken, error) {
ctx := context.Background()
config, err := getOfficeAppInfo()
if err != nil {
return nil, OauthToken{}, err
}
conf := &oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: []string{
"Mail.Read",
"User.Read",
"https://outlook.office.com/mail.read",
},
RedirectURL: "https://localhost:8000",
Endpoint: oauth2.Endpoint{
AuthURL: config.AuthUrl,
TokenURL: config.TokenUrl,
},
}
//"Mail.Read.Shared",
//url := conf.AuthCodeURL("state", oauth2.SetAuthURLParam("resource", "https://outlook.office.com"))
// ADD DATA TO STATE HERE :O
url := conf.AuthCodeURL("workflow_id%3Dc2e0b50a-2957-427e-a97b-b989dc5a5408%26trigger_id%3D9e845679-5843-4959-a76c-a6d664e9df35%26username%3Drheyix.yt@gmail.com", oauth2.SetAuthURLParam("resource", "https://graph.microsoft.com"))
fmt.Printf("Visit the URL for the auth dialog: \n%v\n\n", url)
codechannel := make(chan string)
// Handles the server callback, listening on port 8000
go func() {
port := ":8000"
http.HandleFunc("/", func(response http.ResponseWriter, request *http.Request) {
tmpcode := request.URL.Query().Get("code")
if len(tmpcode) < 100 {
return
} else {
codechannel <- tmpcode
}
})
// FIX - might cause errors not being printed
err := http.ListenAndServeTLS(port, "integrations/server.crt", "integrations/server.key", nil)
if err != nil {
log.Printf("%s\n", err)
}
}()
code := <-codechannel
close(codechannel)
// https://stackoverflow.com/questions/52787420/multiple-resources-in-a-single-authorization-request
// Multi resource ^
access_token, err := conf.Exchange(ctx, code)
//log.Printf("%#v", access_token)
if err != nil {
return nil, OauthToken{}, err
}
//log.Printf("%#v", access_token)
outlookClient := conf.Client(ctx, access_token)
oauthToken := OauthToken{
AccessToken: access_token.AccessToken,
TokenType: access_token.TokenType,
RefreshToken: access_token.RefreshToken,
Expiry: access_token.Expiry,
}
return outlookClient, oauthToken, nil
}
type OauthToken struct {
AccessToken string `json:"AccessToken" datastore:"AccessToken,noindex"`
TokenType string `json:"TokenType" datastore:"TokenType,noindex"`
RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"`
Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"`
}
type Mailfolders struct {
OdataContext string `json:"@odata.context"`
OdataNextLink string `json:"@odata.nextLink"`
Value []struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
ParentFolderID string `json:"parentFolderId"`
ChildFolderCount int `json:"childFolderCount"`
UnreadItemCount int `json:"unreadItemCount"`
TotalItemCount int `json:"totalItemCount"`
} `json:"value"`
}
func getFolders(client *http.Client) (Mailfolders, error) {
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/frikky@shuffletest.onmicrosoft.com/mailfolders")
ret, err := client.Get(requestUrl)
if err != nil {
log.Printf("FolderErr: %s", err)
return Mailfolders{}, err
}
log.Printf("Status folders: %d", ret.StatusCode)
body, err := ioutil.ReadAll(ret.Body)
if err != nil {
log.Printf("Body: %s", err)
return Mailfolders{}, err
}
//log.Printf("Body: %s", string(body))
mailfolders := Mailfolders{}
err = json.Unmarshal(body, &mailfolders)
if err != nil {
log.Printf("Unmarshal: %s", err)
return Mailfolders{}, err
}
//fmt.Printf("%#v", mailfolders)
// FIXME - recursion for subfolders
// Recursive struct
// folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId)
for _, folder := range mailfolders.Value {
log.Println(folder.DisplayName)
}
return mailfolders, nil
}
// Subscribes to a mailbox based on some thingies
func makeSubscription(client *http.Client, folderIds []string) {
// FIXME - show the users folders from oauth and let them choose
fullUrl := "https://graph.microsoft.com/v1.0/subscriptions"
//resource := fmt.Sprintf("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages")
resource := fmt.Sprintf("me/mailfolders('inbox')/messages")
sub := Subscription{
ChangeType: "created",
NotificationURL: "https://de4fc12b.ngrok.io",
ExpirationDateTime: "2019-09-22T18:23:45.9356913Z",
ClientState: "This is a test",
Resource: resource,
}
data, err := json.Marshal(sub)
if err != nil {
log.Printf("Marshal: %s", err)
return
}
log.Printf(string(data))
req, err := http.NewRequest(
"POST",
fullUrl,
bytes.NewBuffer(data),
)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
log.Printf("Client: %s", err)
return
}
log.Printf("Status: %d", res.StatusCode)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Printf("Body: %s", err)
return
}
fmt.Println(string(body))
log.Println("Shoooould be set up :)")
}
func getOutlookClient(code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) {
ctx := context.Background()
conf := &oauth2.Config{
ClientID: "70e37005-c954-4290-b573-d4b94e484336",
ClientSecret: ".eNw/A[kQFB5zL.agvRputdEJENeJ392",
Scopes: []string{
"Mail.Read",
"User.Read",
"https://outlook.office.com/mail.read",
},
RedirectURL: redirectUri,
Endpoint: oauth2.Endpoint{
AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize",
TokenURL: "https://login.microsoftonline.com/common/oauth2/token",
},
}
if len(code) > 0 {
access_token, err := conf.Exchange(ctx, code)
if err != nil {
log.Printf("Access_token issue: %s", err)
return &http.Client{}, access_token, err
}
client := conf.Client(ctx, access_token)
return client, access_token, nil
} else {
// Manually recreate the oauthtoken
access_token := &oauth2.Token{
AccessToken: accessToken.AccessToken,
TokenType: accessToken.TokenType,
RefreshToken: accessToken.RefreshToken,
Expiry: accessToken.Expiry,
}
client := conf.Client(ctx, access_token)
return client, access_token, nil
}
}
func main() {
graphclient, oauthToken, err := get_accesstoken()
if err != nil {
log.Printf("Oauth setup: %s", err)
return
}
// FIXME - make this possible for alternative users (shared)
folders, err := getFolders(graphclient)
if err != nil {
log.Printf("Folder get error: %s", err)
return
}
_ = folders
folderIds := []string{"inbox"}
//log.Println(folders)
//log.Printf("%#v", oauthToken)
// Use oauthToken to generate data for outlook
outlookclient, _, err := getOutlookClient("", oauthToken, "https://localhost:8000")
makeSubscription(outlookclient, folderIds)
}