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" }} + +

Add a resource

+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ {{ .Message }} +
+ +{{ template "footer.html" }} \ No newline at end of file diff --git a/demo/web/tmpl/base/footer.html b/demo/web/tmpl/base/footer.html new file mode 100644 index 0000000..40b7fef --- /dev/null +++ b/demo/web/tmpl/base/footer.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/demo/web/tmpl/base/foulkon-logo.html b/demo/web/tmpl/base/foulkon-logo.html new file mode 100644 index 0000000..dca34e3 --- /dev/null +++ b/demo/web/tmpl/base/foulkon-logo.html @@ -0,0 +1 @@ +data:image/png;base64,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 \ No newline at end of file diff --git a/demo/web/tmpl/base/header.html b/demo/web/tmpl/base/header.html new file mode 100644 index 0000000..2b5592f --- /dev/null +++ b/demo/web/tmpl/base/header.html @@ -0,0 +1,54 @@ + + + + + + Foulkon demo + + + + + + + + + + +
diff --git a/demo/web/tmpl/delete.html b/demo/web/tmpl/delete.html new file mode 100644 index 0000000..d1516c5 --- /dev/null +++ b/demo/web/tmpl/delete.html @@ -0,0 +1,19 @@ +{{ template "header.html" }} + +

Remove a resource

+
+
+
+ + +
+
+ +
+
+
+
+ {{ .Message }} +
+ +{{ template "footer.html" }} \ No newline at end of file diff --git a/demo/web/tmpl/error.html b/demo/web/tmpl/error.html new file mode 100644 index 0000000..fd73e54 --- /dev/null +++ b/demo/web/tmpl/error.html @@ -0,0 +1,11 @@ +{{ template "header.html" }} +
+ + {{.HttpErrorStatusCode}} + + + {{.ErrorMessage}} + +
+ +{{ template "footer.html" }} \ No newline at end of file diff --git a/demo/web/tmpl/index.html b/demo/web/tmpl/index.html new file mode 100644 index 0000000..e68c58c --- /dev/null +++ b/demo/web/tmpl/index.html @@ -0,0 +1,42 @@ +{{ template "header.html" }} +

Foulkon Example Demo

+
+ {{if eq .Token ""}} + You must login before you use endpoints to work with example resources. Click here to log in. + {{else}} +
+
+ User identifier: + 123123213123123123321 +
+
+
+
+ Email: + {{.Email}} +
+
+ + + + + + + {{ $length := len .Roles }} {{ if eq $length 0 }} + + + + {{else}} + {{range $element := .Roles}} + + + + + + {{end}} + {{end}} +
RoleOrganizationJoined
No roles yet
{{ $element.Name }}{{ $element.Org }}{{ $element.CreateAt.Format "Jan 02, 2006 15:04:05 UTC" }}
+ {{end}} +
+ +{{ template "footer.html" }} \ No newline at end of file diff --git a/demo/web/tmpl/list.html b/demo/web/tmpl/list.html new file mode 100644 index 0000000..eb80714 --- /dev/null +++ b/demo/web/tmpl/list.html @@ -0,0 +1,30 @@ +{{ template "header.html" }} + +
+

Resources

+ {{ $length := len .ResourceTableElements.Resources }} + {{ if eq $length 0 }} +
+ List empty +
+ {{else}} + + + + + + + + + {{range $element := .ResourceTableElements.Resources}} + + + + + {{end}} + +
IDResource
{{ $element.Id }}{{ $element.Resource }}
+ {{end}} +
+ +{{ template "footer.html" }} \ No newline at end of file diff --git a/demo/web/tmpl/update.html b/demo/web/tmpl/update.html new file mode 100644 index 0000000..571d86c --- /dev/null +++ b/demo/web/tmpl/update.html @@ -0,0 +1,24 @@ +{{ template "header.html" }} + +

Update a resource

+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ {{ .Message }} +
+ + +{{ template "footer.html" }} \ No newline at end of file diff --git a/glide.lock b/glide.lock index a8e6bef..b03ba99 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,22 @@ -hash: 93d9eba9da07ecb1e8de03b355a36f05d853193e3107852c68713fd7aac5fd3c -updated: 2017-09-04T10:16:05.262507613+02:00 +hash: c1aade463908be588aa9570dba8584c95269d410cd753f34729a75fd19f153cc +updated: 2018-02-05T15:54:15.97488+01:00 imports: +- name: github.com/coreos/go-oidc + version: a93f71fdfe73d2c0f5413c0565eea0af6523a6df + subpackages: + - http + - jose + - key + - oauth2 + - oidc +- name: github.com/coreos/pkg + version: 97fdf19511ea361ae1c100dd393cc47f8dcfa1e1 + subpackages: + - health + - httputil + - timeutil - name: github.com/dgrijalva/jwt-go - version: 24c63f56522a87ec5339cc3567883f1039378fdb + version: dbeaa9332f19a944acb5736b4456cfcc02140e29 - name: github.com/emanoelxavier/openid2go version: efe3c34772c5a961048a05e9483da2bd24debed0 subpackages: @@ -10,7 +24,9 @@ imports: - name: github.com/jinzhu/gorm version: 5174cc5c242a728b435ea2be8a2f7f998e15429b - name: github.com/jinzhu/inflection - version: 74387dc39a75e970e7a3ae6a3386b5bd2e5c5cff + version: 1c35d901db3da928c72a72d8458480cc9ade058f +- name: github.com/jonboulle/clockwork + version: bcac9884e7502bb2b474c0339d889cb981a2f27f - name: github.com/julienschmidt/httprouter version: 8c199fb6259ffc1af525cc3ad52ee60ba8359669 - name: github.com/kylelemons/godebug @@ -29,11 +45,11 @@ imports: - name: github.com/satori/go.uuid version: 879c5887cd475cd7864858769793b2ceb0d44feb - name: github.com/sirupsen/logrus - version: f006c2ac4710855cf0f916dd6b77acf6b048dc6e + version: d682213848ed68c0a260ca37d6dd5ace8423f5ba subpackages: - hooks/test - name: github.com/square/go-jose - version: 789a4c4bd4c118f7564954f441b29c153ccd6a96 + version: 0210e50945bbf0685c3313c71cd243f0870b96e3 subpackages: - cipher - json @@ -42,13 +58,14 @@ imports: subpackages: - assert - name: golang.org/x/crypto - version: 1fbbd62cfec66bd39d91e97749579579d4d3037e + version: 1875d0a70c90e57f11972aefd42276df65e895b9 subpackages: - ssh/terminal - name: golang.org/x/sys - version: c200b10b5d5e122be351b67af224adc6128af5bf + version: 37707fdb30a5b38865cfb95e5aab41707daec7fd subpackages: - unix + - windows testImports: - name: github.com/davecgh/go-spew version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 diff --git a/glide.yaml b/glide.yaml index 96e81f9..e8e7cb7 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,7 +1,7 @@ package: github.com/Tecsisa/foulkon import: - package: github.com/sirupsen/logrus - version: 1.0.3 + version: 1.0.4 - package: github.com/julienschmidt/httprouter version: 1.1 - package: github.com/lib/pq @@ -20,3 +20,5 @@ import: version: d65d576e9348f5982d7f6d83682b694e731a45c6 - package: github.com/stretchr/testify version: 1.1.4 +- package: github.com/coreos/go-oidc + version: a93f71fdfe73d2c0f5413c0565eea0af6523a6df diff --git a/scripts/test.sh b/scripts/test.sh index acd1c1c..0fa0c69 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,7 +7,7 @@ echo "" > coverage.txt echo "--> Running tests" echo -e '----> Running unit tests' -for d in $(go list ./... | grep -v '/vendor/' | egrep -v '/database/|cmd/|auth/oidc|foulkon/foulkon'); do +for d in $(go list ./... | grep -v '/vendor/' | egrep -v '/database/|demo/|cmd/|auth/oidc|foulkon/foulkon'); do go test -race -coverprofile=profile.out -covermode=atomic $d || exit 1 if [ -f profile.out ]; then cat profile.out >> coverage.txt