diff --git a/.travis.yml b/.travis.yml index 594d429..b9af00a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ sudo: required -dist: xenial services: - docker diff --git a/GNUmakefile b/GNUmakefile index 1c99194..6895f0c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -19,6 +19,9 @@ deps: bin: generate format imports @sh -c "'$(PWD)/scripts/build.sh'" +demo: + @sh -c "'$(PWD)/demo/build-demo.sh'" + release: @$(MAKE) bin @@ -74,4 +77,4 @@ bootstrap: deps travis: @sh -c "'$(PWD)/scripts/travis.sh'" -.PHONY: all dev deps bin release generate format imports test vet bootstrap travis +.PHONY: all dev deps bin demo release generate format imports test vet bootstrap travis diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..85321eb --- /dev/null +++ b/demo/README.md @@ -0,0 +1,89 @@ +# Foulkon demo + +This demo shows how Foulkon works, and how to manage it. + +## Previous requirements + +To run this demo, you have to set some properties and system packages. + +### Go configuration + +We have to set next environment vars: + + - GOROOT + - GOPATH + - GOBIN + +On Ubuntu (Directory examples, you can choose your own directories): + +```bash + +export GOROOT=$HOME/dev/golang/go +export GOPATH=$HOME/dev/sources/golang +export GOBIN=$HOME/dev/sources/golang/bin + +``` + +This directories will be created before. Also, the GOBIN environment variable +will be in your execution path. + +### System packages + +This demo works with Docker, so you have to install Docker and Docker Compose. + + - [Docker installation doc](https://docs.docker.com/engine/installation/) + - [Docker Compose installation doc](https://docs.docker.com/compose/install/) + +## Start Demo + +First, you have to download Foulkon project: + +```bash + +go get github.com/Tecsisa/foulkon + +``` + +Second, go to Foulkon directory: + +```bash + +cd $GOPATH/src/github.com/Tecsisa/foulkon + +``` + +Third, execute next command to get all dependencies: + +```bash + +make bootstrap + +``` + +User login needs a Google client to make UI able to get a user. +To do this, follow the [Google guide](https://developers.google.com/identity/protocols/OpenIDConnect) to get a Google client +set http://localhost:8101/callback in your Authorized redirect URIs, and change next properties in [Docker-compose file](docker/docker-compose.yml): + + - In foulkonworkercompose: + - FOULKON_AUTH_CLIENTID for your client id. + - In foulkondemowebcompose: + - OIDC_CLIENT_ID for your client id. + - OIDC_CLIENT_SECRET for your secret. + +Finally, execute demo command to start demo: + +```bash + +make bootstrap + +``` + +The applications started are next: + + - Worker: Started on http://localhost:8000 + - Proxy: Started on http://localhost:8001 + - API demo: Started on http://localhost:8100 + - UI demo: Started on http://localhost:8101 + +Now, you have all suite to try Foulkon, go to [Tour](tour.md) to see an example. + diff --git a/demo/api/main.go b/demo/api/main.go new file mode 100644 index 0000000..fa394ba --- /dev/null +++ b/demo/api/main.go @@ -0,0 +1,274 @@ +package main + +import ( + "encoding/json" + "net/http" + "os" + + "bytes" + + "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" +) + +// CONSTANTS +const ( + // Environment Vars + HOST = "APIHOST" + PORT = "APIPORT" + FOULKONHOST = "FOULKON_WORKER_HOST" + FOULKONPORT = "FOULKON_WORKER_PORT" + + // HTTP Constants + RESOURCE_ID = "id" +) + +type Resource struct { + Id string `json:"id, omitempty"` + Resource string `json:"resource, omitempty"` +} + +var resources map[string]string +var logger *logrus.Logger + +func HandleAddResource(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + request := &Resource{} + err := processHttpRequest(r, request) + var response *Resource + if err == nil { + resources[request.Id] = request.Resource + response = request + } + processHttpResponse(w, response, err, http.StatusCreated) +} + +func HandleGetResource(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) { + var response *Resource + var statusCode int + if val, ok := resources[ps.ByName(RESOURCE_ID)]; ok { + response = &Resource{ + Id: ps.ByName(RESOURCE_ID), + Resource: val, + } + statusCode = http.StatusOK + } else { + statusCode = http.StatusNotFound + } + processHttpResponse(w, response, nil, statusCode) +} + +func HandlePutResource(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + request := &Resource{} + err := processHttpRequest(r, request) + var response *Resource + if err == nil { + id := ps.ByName("id") + resources[id] = request.Resource + response = request + } + processHttpResponse(w, response, err, http.StatusOK) +} + +func HandleDelResource(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) { + var statusCode int + if _, ok := resources[ps.ByName(RESOURCE_ID)]; ok { + delete(resources, ps.ByName(RESOURCE_ID)) + statusCode = http.StatusNoContent + } else { + statusCode = http.StatusNotFound + } + processHttpResponse(w, nil, nil, statusCode) +} + +func HandleListResources(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + response := make([]Resource, len(resources)) + for key, val := range resources { + response = append(response, Resource{Id: key, Resource: val}) + } + processHttpResponse(w, response, nil, http.StatusOK) +} + +func main() { + + logger = &logrus.Logger{ + Out: os.Stdout, + Formatter: &logrus.JSONFormatter{}, + Hooks: make(logrus.LevelHooks), + Level: logrus.InfoLevel, + } + + // Startup roles + createRolesAndPolicies() + + // Create the muxer to handle the actual endpoints + router := httprouter.New() + + router.POST("/resources", HandleAddResource) + router.GET("/resources/:id", HandleGetResource) + router.PUT("/resources/:id", HandlePutResource) + router.DELETE("/resources/:id", HandleDelResource) + router.GET("/resources", HandleListResources) + + // Start server + resources = make(map[string]string) + host := os.Getenv(HOST) + port := os.Getenv(PORT) + logger.Infof("Server running in %v:%v", host, port) + logger.Fatal(http.ListenAndServe(host+":"+port, router)) + +} + +// Private Helper Methods + +func processHttpRequest(r *http.Request, request interface{}) error { + // Decode request if passed + if request != nil { + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + return err + } + } + + return nil +} + +func processHttpResponse(w http.ResponseWriter, response interface{}, err error, responseCode int) { + if err != nil { + http.Error(w, err.Error(), responseCode) + return + } + + var data []byte + if response != nil { + data, err = json.Marshal(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + w.WriteHeader(responseCode) + + switch responseCode { + case http.StatusOK: + w.Write(data) + case http.StatusCreated: + w.Write(data) + } + +} + +func createRolesAndPolicies() { + foulkonhost := os.Getenv(FOULKONHOST) + foulkonport := os.Getenv(FOULKONPORT) + url := "http://" + foulkonhost + ":" + foulkonport + "/api/v1" + + createGroupFunc := func(name, path string) error { + + type CreateGroupRequest struct { + Name string `json:"name, omitempty"` + Path string `json:"path, omitempty"` + } + var body *bytes.Buffer + data := &CreateGroupRequest{ + Name: name, + Path: path, + } + + jsonObject, _ := json.Marshal(data) + body = bytes.NewBuffer(jsonObject) + + req, _ := http.NewRequest(http.MethodPost, url+"/organizations/demo/groups", body) + req.SetBasicAuth("admin", "admin") + + _, err := http.DefaultClient.Do(req) + return err + } + + type Statement struct { + Effect string `json:"effect, omitempty"` + Actions []string `json:"actions, omitempty"` + Resources []string `json:"resources, omitempty"` + } + + createPolicyAndAttachToGroup := func(name, path, groupName string, statements []Statement) error { + client := http.DefaultClient + type CreatePolicyRequest struct { + Name string `json:"name, omitempty"` + Path string `json:"path, omitempty"` + Statements []Statement `json:"statements, omitempty"` + } + var body *bytes.Buffer + data := &CreatePolicyRequest{ + Name: name, + Path: path, + Statements: statements, + } + + jsonObject, _ := json.Marshal(data) + body = bytes.NewBuffer(jsonObject) + + req, _ := http.NewRequest(http.MethodPost, url+"/organizations/demo/policies", body) + req.SetBasicAuth("admin", "admin") + + _, err := client.Do(req) + if err != nil { + return err + } + + // Attach + req, _ = http.NewRequest(http.MethodPost, url+"/organizations/demo/groups/"+groupName+"/policies/"+name, nil) + req.SetBasicAuth("admin", "admin") + + _, err = client.Do(req) + if err != nil { + return err + } + + return nil + } + + // Create read role + err := createGroupFunc("read", "/path/") + if err != nil { + logger.Fatal(err) + } + statements := []Statement{ + { + Effect: "allow", + Actions: []string{"example:list", "example:get"}, + Resources: []string{ + "urn:ews:foulkon:demo1:resource/list", + "urn:ews:foulkon:demo1:resource/demoresources/*", + }, + }, + } + + err = createPolicyAndAttachToGroup("read-policy", "/path/", "read", statements) + if err != nil { + logger.Fatal(err) + } + + // Create read write role + err = createGroupFunc("read-write", "/path/") + if err != nil { + logger.Fatal(err) + } + statements2 := []Statement{ + { + Effect: "allow", + Actions: []string{"example:list", "example:get", "example:add", "example:update", "example:delete"}, + Resources: []string{ + "urn:ews:foulkon:demo1:resource/list", + "urn:ews:foulkon:demo1:resource/demoresources/*", + "urn:ews:foulkon:demo1:resource/add", + }, + }, + } + + err = createPolicyAndAttachToGroup("read-write-policy", "/path/", "read-write", statements2) + if err != nil { + logger.Fatal(err) + } + +} diff --git a/demo/build-demo.sh b/demo/build-demo.sh new file mode 100755 index 0000000..344786b --- /dev/null +++ b/demo/build-demo.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +echo "----> Building Demo..." + +echo "----> Compiling demo example apps..." +# Make sure $GOPATH is set +CGO_ENABLED=0 go install github.com/Tecsisa/foulkon/demo/api || exit 1 +CGO_ENABLED=0 go install github.com/Tecsisa/foulkon/demo/web || exit 1 + +echo "----> Moving compiled files to GOROOT path..." +mkdir bin/ 2>/dev/null +cp $GOPATH/bin/api ./bin +cp $GOPATH/bin/web ./bin + +echo "----> Building Docker demo images..." +docker build -t tecsisa/foulkondemo -f demo/docker/Dockerfile . >/dev/null || exit 1 + +echo "----> Starting Docker Compose..." +docker-compose -f demo/docker/docker-compose.yml up --force-recreate --abort-on-container-exit \ No newline at end of file diff --git a/demo/docker/Dockerfile b/demo/docker/Dockerfile new file mode 100644 index 0000000..3b25cfb --- /dev/null +++ b/demo/docker/Dockerfile @@ -0,0 +1,22 @@ +FROM tecsisa/foulkon:v0.3.0 +MAINTAINER Tecsisa + +# Copy resources of this API +COPY demo/docker/demo-proxy.toml /proxy.toml + +# API +COPY bin/api /go/bin/api + +# WEB +COPY bin/web /go/bin/web +COPY demo/web/tmpl /tmpl + +# Entrypoint +ADD demo/docker/entrypoint.sh /go/bin/entrypoint.sh +RUN chmod 750 /go/bin/* + +ENV PATH=$PATH:/go/bin + +EXPOSE 8000 8001 8100 8101 + +ENTRYPOINT ["/go/bin/entrypoint.sh"] \ No newline at end of file diff --git a/demo/docker/demo-proxy.toml b/demo/docker/demo-proxy.toml new file mode 100644 index 0000000..eae2d17 --- /dev/null +++ b/demo/docker/demo-proxy.toml @@ -0,0 +1,52 @@ +# Server config +[server] +host = "${FOULKON_PROXY_HOST}" +port = "${FOULKON_PROXY_PORT}" +certfile = "${FOULKON_PROXY_CERT_FILE_PATH}" +keyfile = "${FOULKON_PROXY_KEY_FILE_PATH}" +worker-host = "${FOULKON_WORKER_URL}" + +# Logger +[logger] +type = "${FOULKON_PROXY_LOG_TYPE}" +level = "debug" + # Directory for file configuration + [logger.file] + dir = "${FOULKON_PROXY_LOG_PATH}" + +# Resources definition example +[[resources]] + id = "resource1" + host = "http://foulkondemoapicompose:8100" + url = "/resources" + method = "GET" + urn = "urn:ews:foulkon:demo1:resource/list" + action = "example:list" +[[resources]] + id = "resource2" + host = "http://foulkondemoapicompose:8100" + url = "/resources" + method = "POST" + urn = "urn:ews:foulkon:demo1:resource/add" + action = "example:add" +[[resources]] + id = "resource3" + host = "http://foulkondemoapicompose:8100" + url = "/resources/:id" + method = "GET" + urn = "urn:ews:foulkon:demo1:resource/demoresources/{id}" + action = "example:get" +[[resources]] + id = "resource4" + host = "http://foulkondemoapicompose:8100" + url = "/resources/:id" + method = "PUT" + urn = "urn:ews:foulkon:demo1:resource/demoresources/{id}" + action = "example:update" +[[resources]] + id = "resource5" + host = "http://foulkondemoapicompose:8100" + url = "/resources/:id" + method = "DELETE" + urn = "urn:ews:foulkon:demo1:resource/demoresources/{id}" + action = "example:delete" \ No newline at end of file diff --git a/demo/docker/docker-compose.yml b/demo/docker/docker-compose.yml new file mode 100644 index 0000000..79201b0 --- /dev/null +++ b/demo/docker/docker-compose.yml @@ -0,0 +1,86 @@ +version: "2" +services: + postgrescompose: + image: "postgres:9.5" + container_name: "postgrescompose" + hostname: "postgrescompose" + command: "postgres" + foulkonworkercompose: + image: "tecsisa/foulkon:v0.4.0" + container_name: "foulkonworkercompose" + hostname: "foulkonworkercompose" + environment: + - FOULKON_WORKER_HOST=foulkonworkercompose + - FOULKON_WORKER_PORT=8000 + - FOULKON_ADMIN_USER=admin + - FOULKON_ADMIN_PASS=admin + - FOULKON_WORKER_LOG_TYPE=default + - FOULKON_WORKER_LOG_LEVEL=info + - FOULKON_DB=postgres + - FOULKON_DB_POSTGRES_DS=postgres://postgres:password@postgrescompose:5432/postgres?sslmode=disable + - FOULKON_DB_POSTGRES_IDLECONNS=10 + - FOULKON_DB_POSTGRES_MAXCONNS=20 + - FOULKON_DB_POSTGRES_CONNTTL=300 + - FOULKON_AUTH_TYPE=oidc + ports: + - "8000:8000" + command: "worker" + depends_on: + - postgrescompose + foulkondemoproxycompose: + image: "tecsisa/foulkon:v0.4.0" + container_name: "foulkondemoproxycompose" + hostname: "foulkondemoproxycompose" + environment: + - FOULKON_PROXY_HOST=foulkondemoproxycompose + - FOULKON_PROXY_PORT=8001 + - FOULKON_PROXY_LOG_TYPE=default + - FOULKON_PROXY_LOG_LEVEL=info + - FOULKON_WORKER_URL=http://foulkonworkercompose:8000 + - FOULKON_DB=postgres + - FOULKON_DB_POSTGRES_DS=postgres://postgres:password@postgrescompose:5432/postgres?sslmode=disable + - FOULKON_DB_POSTGRES_IDLECONNS=10 + - FOULKON_DB_POSTGRES_MAXCONNS=20 + - FOULKON_DB_POSTGRES_CONNTTL=300 + - FOULKON_RESOURCES_REFRESH=10s + ports: + - "8001:8001" + command: "proxy" + depends_on: + - postgrescompose + - foulkonworkercompose + foulkondemoapicompose: + image: "tecsisa/foulkondemo" + container_name: "foulkondemoapicompose" + hostname: "foulkondemoapicompose" + environment: + - FOULKON_WORKER_HOST=foulkonworkercompose + - FOULKON_WORKER_PORT=8000 + - APIHOST=foulkondemoapicompose + - APIPORT=8100 + ports: + - "8100:8100" + command: "api" + depends_on: + - postgrescompose + - foulkonworkercompose + foulkondemowebcompose: + image: "tecsisa/foulkondemo" + container_name: "foulkondemowebcompose" + hostname: "foulkondemowebcompose" + environment: + - APIHOST=foulkondemoproxycompose + - APIPORT=8001 + - WEBHOST=localhost + - WEBPORT=8101 + - FOULKON_WORKER_HOST=foulkonworkercompose + - FOULKON_WORKER_PORT=8000 + - OIDC_CLIENT_ID=GoogleClientId + - OIDC_CLIENT_SECRET=GoogleClientSecret + - OIDC_IDP_DISCOVERY=https://accounts.google.com + ports: + - "8101:8101" + command: "web" + depends_on: + - postgrescompose + - foulkonworkercompose \ No newline at end of file diff --git a/demo/docker/entrypoint.sh b/demo/docker/entrypoint.sh new file mode 100644 index 0000000..57df9f2 --- /dev/null +++ b/demo/docker/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +usage() { echo "Usage: proxy|api|web" 1>&2; exit 1; } +if [ "$1" = 'proxy' ]; then + proxy -proxy-file=/proxy.toml +elif [ "$1" = 'api' ]; then + api +elif [ "$1" = 'web' ]; then + web +else + usage +fi \ No newline at end of file diff --git a/demo/tour.md b/demo/tour.md new file mode 100644 index 0000000..e213551 --- /dev/null +++ b/demo/tour.md @@ -0,0 +1,3 @@ +# Foulkon tour + +WIP \ No newline at end of file diff --git a/demo/web/main.go b/demo/web/main.go new file mode 100644 index 0000000..9a43908 --- /dev/null +++ b/demo/web/main.go @@ -0,0 +1,552 @@ +package main + +import ( + "bytes" + "encoding/json" + "html/template" + "log" + "net/http" + "os" + + "fmt" + "net/url" + + "time" + + "github.com/coreos/go-oidc/oidc" + "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" +) + +// CONSTANTS +const ( + // Environment Vars + WEBHOST = "WEBHOST" + WEBPORT = "WEBPORT" + APIHOST = "APIHOST" + APIPORT = "APIPORT" + + // Foulkon + FOULKONHOST = "FOULKON_WORKER_HOST" + FOULKONPORT = "FOULKON_WORKER_PORT" + + // OIDC + OIDCCLIENTID = "OIDC_CLIENT_ID" + OIDCCLIENTSECRET = "OIDC_CLIENT_SECRET" + OIDCIDPDISCOVERY = "OIDC_IDP_DISCOVERY" +) + +type Node struct { + // URLs + WebBaseUrl string + APIBaseUrl string + FoulkonBaseUrl string + + // Profile + UserId string + Email string + Token string + Roles []UserGroups + + // Table Resources + ResourceTableElements *ResourceTableElements + + // Msg + Message string + + // Error + HttpErrorStatusCode int + ErrorMessage string +} + +type Resource struct { + Id string `json:"id, omitempty"` + Resource string `json:"resource, omitempty"` +} + +type UserGroups struct { + Org string `json:"org, omitempty"` + Name string `json:"name, omitempty"` + CreateAt time.Time `json:"joined, omitempty"` +} + +type GetGroupsByUserIdResponse struct { + Groups []UserGroups `json:"groups, omitempty"` + Limit int `json:"limit, omitempty"` + Offset int `json:"offset, omitempty"` + Total int `json:"total, omitempty"` +} + +type UpdateResource struct { + Resource string `json:"resource, omitempty"` +} + +type ResourceTableElements struct { + Resources []Resource +} + +var mainTemplate *template.Template +var listTemplate *template.Template +var addTemplate *template.Template +var removeTemplate *template.Template +var updateTemplate *template.Template +var errorTemplate *template.Template +var client = http.DefaultClient +var logger *logrus.Logger +var node = new(Node) + +func main() { + // Get API Location + apiHost := os.Getenv(APIHOST) + apiPort := os.Getenv(APIPORT) + apiURL := "http://" + apiHost + ":" + apiPort + node.APIBaseUrl = apiURL + + // Get Web Location + host := os.Getenv(WEBHOST) + port := os.Getenv(WEBPORT) + webURL := "http://" + host + ":" + port + node.WebBaseUrl = webURL + + // Get foulkon url + foulkonhost := os.Getenv(FOULKONHOST) + foulkonport := os.Getenv(FOULKONPORT) + node.FoulkonBaseUrl = "http://" + foulkonhost + ":" + foulkonport + "/api/v1" + + logger = &logrus.Logger{ + Out: os.Stdout, + Formatter: &logrus.JSONFormatter{}, + Hooks: make(logrus.LevelHooks), + Level: logrus.InfoLevel, + } + + // Create oidc client + client, err := createOidcClient(host, port) + if err != nil { + logger.Fatalf("There was an error creating oidc client: %v", err) + } + + if client == nil { + logger.Fatal("Nil OIDC client") + } + + // Create templates + createTemplates() + + router := httprouter.New() + router.GET("/", HandlePage) + router.POST("/", HandlePage) + router.GET("/add", HandleAddResource) + router.POST("/add", HandleAddResource) + router.GET("/remove", HandleRemoveResource) + router.POST("/remove", HandleRemoveResource) + router.GET("/update", HandleUpdateResource) + router.POST("/update", HandleUpdateResource) + router.GET("/list", HandleListResources) + router.GET("/login", HandleLoginFunc(client)) + router.GET("/callback", HandleCallbackFunc(client)) + + // Start server + logger.Infof("Server running in %v:%v", host, port) + log.Fatal(http.ListenAndServe(":"+port, router)) +} + +// HANDLERS + +func HandlePage(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + if node.Token != "" { + request, err := http.NewRequest(http.MethodGet, node.FoulkonBaseUrl+"/users/"+node.UserId+"/groups", nil) + if err != nil { + logger.Error(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + request.SetBasicAuth("admin", "admin") + + response, err := client.Do(request) + if err != nil { + logger.Error(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + err := mainTemplate.Execute(w, node) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + buffer := new(bytes.Buffer) + if _, err := buffer.ReadFrom(response.Body); err != nil { + logger.Error(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := new(GetGroupsByUserIdResponse) + if err := json.Unmarshal(buffer.Bytes(), &res); err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + node.Roles = res.Groups + + } + err := mainTemplate.Execute(w, node) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func HandleListResources(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + request, err := http.NewRequest(http.MethodGet, node.APIBaseUrl+"/resources", nil) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + request.Header.Set("Authorization", "Bearer "+node.Token) + + response, err := client.Do(request) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + renderErrorTemplate(w, response.StatusCode, fmt.Sprintf("List method error. Error code: %v", http.StatusText(response.StatusCode))) + return + } + + buffer := new(bytes.Buffer) + if _, err := buffer.ReadFrom(response.Body); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := make([]Resource, 0) + if err := json.Unmarshal(buffer.Bytes(), &res); err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + node.ResourceTableElements = &ResourceTableElements{ + Resources: res, + } + + node.Message = "" + err = listTemplate.Execute(w, node) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func HandleAddResource(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if r.Method == http.MethodPost { + logger.Info("Adding resource") + r.ParseForm() + res := Resource{ + Id: r.Form.Get("id"), + Resource: r.Form.Get("resource"), + } + + jsonObject, err := json.Marshal(&res) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + body := bytes.NewBuffer(jsonObject) + + request, err := http.NewRequest(http.MethodPost, node.APIBaseUrl+"/resources", body) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + request.Header.Set("Authorization", "Bearer "+node.Token) + + response, err := client.Do(request) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if response.StatusCode != http.StatusCreated { + + renderErrorTemplate(w, response.StatusCode, fmt.Sprintf("Add method error. Error code: %v", http.StatusText(response.StatusCode))) + return + } + + node.Message = fmt.Sprintf("Resource with id %v and resource %v was created!", res.Id, res.Resource) + + addTemplate.Execute(w, node) + } else { + node.Message = "" + err := addTemplate.Execute(w, node) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +func HandleUpdateResource(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if r.Method == http.MethodPost { + logger.Info("Updating resource") + r.ParseForm() + res := UpdateResource{ + Resource: r.Form.Get("resource"), + } + id := r.Form.Get("id") + + jsonObject, err := json.Marshal(&res) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + body := bytes.NewBuffer(jsonObject) + + request, err := http.NewRequest(http.MethodPut, node.APIBaseUrl+"/resources/"+id, body) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + request.Header.Set("Authorization", "Bearer "+node.Token) + + response, err := client.Do(request) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if response.StatusCode != http.StatusOK { + renderErrorTemplate(w, response.StatusCode, fmt.Sprintf("Update method not available. Error code: %v", http.StatusText(response.StatusCode))) + return + } + + node.Message = fmt.Sprintf("Resource with id %v was updated to resource %v!", id, res.Resource) + updateTemplate.Execute(w, node) + } else { + node.Message = "" + err := updateTemplate.Execute(w, node) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +func HandleRemoveResource(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if r.Method == http.MethodPost { + logger.Info("Removing resource") + r.ParseForm() + id := r.Form.Get("id") + + request, err := http.NewRequest(http.MethodDelete, node.APIBaseUrl+"/resources/"+id, nil) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + request.Header.Set("Authorization", "Bearer "+node.Token) + response, err := client.Do(request) + + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if response.StatusCode != http.StatusNoContent { + renderErrorTemplate(w, response.StatusCode, fmt.Sprintf("Remove method not available. Error code: %v", http.StatusText(response.StatusCode))) + return + } + + node.Message = fmt.Sprintf("Resource with id %v was removed", id) + removeTemplate.Execute(w, node) + } else { + node.Message = "" + err := removeTemplate.Execute(w, node) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + } +} + +func HandleLoginFunc(c *oidc.Client) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + oac, err := c.OAuthClient() + if err != nil { + panic("unable to proceed") + } + + u, err := url.Parse(oac.AuthCodeURL("", "", "")) + if err != nil { + panic("unable to proceed") + } + http.Redirect(w, r, u.String(), http.StatusFound) + } +} + +func HandleCallbackFunc(c *oidc.Client) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + code := r.URL.Query().Get("code") + if code == "" { + renderErrorTemplate(w, http.StatusBadRequest, fmt.Sprint("code query param must be set")) + return + } + + tok, err := c.ExchangeAuthCode(code) + if err != nil { + renderErrorTemplate(w, http.StatusBadRequest, fmt.Sprintf("unable to verify auth code with issuer: %v", err)) + return + } + + claims, err := tok.Claims() + if err != nil { + renderErrorTemplate(w, http.StatusBadRequest, fmt.Sprintf("unable to construct claims: %v", err)) + return + } + + node.Token = tok.Encode() + node.UserId, _, _ = claims.StringClaim("sub") + node.Email, _, _ = claims.StringClaim("email") + + http.Redirect(w, r, node.WebBaseUrl, http.StatusFound) + } +} + +// Aux methods + +func createTemplates() { + var err error + mainTemplate, err = template.ParseGlob("tmpl/index.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + mainTemplate, err = mainTemplate.ParseGlob("tmpl/base/*.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + listTemplate, err = template.ParseGlob("tmpl/list.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + listTemplate, err = listTemplate.ParseGlob("tmpl/base/*.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + addTemplate, err = template.ParseGlob("tmpl/add.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + addTemplate, err = addTemplate.ParseGlob("tmpl/base/*.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + removeTemplate, err = template.ParseGlob("tmpl/delete.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + removeTemplate, err = removeTemplate.ParseGlob("tmpl/base/*.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + updateTemplate, err = template.ParseGlob("tmpl/update.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + updateTemplate, err = updateTemplate.ParseGlob("tmpl/base/*.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + errorTemplate, err = template.ParseGlob("tmpl/error.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } + + errorTemplate, err = errorTemplate.ParseGlob("tmpl/base/*.html") + if err != nil { + log.Fatalf("Template can't be parsed: %v", err) + } +} + +func createOidcClient(host, port string) (*oidc.Client, error) { + // OIDC client basics + redirectURL := "http://" + host + ":" + port + "/callback" + discovery := os.Getenv(OIDCIDPDISCOVERY) + + // OIDC client credentials + cc := oidc.ClientCredentials{ + ID: os.Getenv(OIDCCLIENTID), + Secret: os.Getenv(OIDCCLIENTSECRET), + } + + logger.Infof("Configured OIDC client with values: redirectURL: %v, IDP discovery: %v", redirectURL, discovery) + + var cfg oidc.ProviderConfig + var err error + /*for {*/ + cfg, err = oidc.FetchProviderConfig(http.DefaultClient, discovery) + if err != nil { + return nil, err + } + /* + sleep := 3 * time.Second + logger.Infof("failed fetching provider config, trying again in %v: %v", sleep, err) + time.Sleep(sleep) + }*/ + + logger.Infof("fetched provider config from %s: %#v", discovery, cfg) + + ccfg := oidc.ClientConfig{ + ProviderConfig: cfg, + Credentials: cc, + RedirectURL: redirectURL, + } + + oidcClient, err := oidc.NewClient(ccfg) + if err != nil { + return nil, err + } + + oidcClient.SyncProviderConfig(discovery) + + return oidcClient, nil + +} + +func renderErrorTemplate(w http.ResponseWriter, code int, msg string) { + node.HttpErrorStatusCode = code + node.ErrorMessage = msg + err := errorTemplate.Execute(w, node) + if err != nil { + logger.Info(err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/demo/web/tmpl/add.html b/demo/web/tmpl/add.html new file mode 100644 index 0000000..d88f2f3 --- /dev/null +++ b/demo/web/tmpl/add.html @@ -0,0 +1,23 @@ +{{ template "header.html" }} + +