├── .gitignore ├── LICENSE ├── README.md ├── opa-auth ├── go.mod ├── go.sum ├── handler.go └── policy │ └── example.rego └── stack.yml /.gitignore: -------------------------------------------------------------------------------- 1 | template/ 2 | build/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Adaptant Solutions AG 4 | Copyright (c) 2019 Paul Mundt 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openfaas-function-auth-opa 2 | 3 | This repository provides an example of [Open Policy Agent](https://www.openpolicyagent.org/)\-backed authentication in OpenFaaS Serverless functions. 4 | 5 | ## Quick Start 6 | 7 | To try it out, you will need to have an OPA server in your OpenFaaS stack. A version implementing this by default can 8 | be found [here](https://github.com/adaptant-labs/faas/tree/opa-integration). Once this is up and running, fetch the 9 | [golang-http-gomod](https://github.com/adaptant-labs/openfaas-golang-http-gomod-template) template and deploy as normal: 10 | 11 | ``` 12 | $ faas-cli template pull https://github.com/adaptant-labs/openfaas-golang-http-gomod-template.git 13 | $ faas-cli up --skip-push 14 | ``` 15 | ## Example Policy 16 | 17 | A simple example rego policy is provided in order to get started. This policy 18 | prohibits access by default, allowing access to the named function only for a 19 | specified user: 20 | 21 | ``` 22 | package openfaas.authz 23 | 24 | default allow = false 25 | 26 | allow { 27 | input.function == "opa-auth" 28 | input.user == "alice" 29 | } 30 | ``` 31 | 32 | ## Function Invocation 33 | 34 | Invocation of the function is prohibited by default by the example policy: 35 | 36 | ``` 37 | $ curl -X POST http://127.0.0.1:8080/function/opa-auth 38 | Unauthorized. 39 | ``` 40 | 41 | Retrying the request with the permitted named user succeeds: 42 | 43 | ``` 44 | $ curl -H 'Authorization: alice' -X POST http://127.0.0.1:8080/function/opa-auth 45 | Authorization OK 46 | ``` 47 | 48 | ## Licensing 49 | 50 | Released under the terms of the [MIT](LICENSE) license. 51 | -------------------------------------------------------------------------------- /opa-auth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adaptant-labs/openfaas-function-auth-opa/opa-auth 2 | 3 | go 1.13 4 | 5 | require github.com/adaptant-labs/go-function-sdk v0.0.0-20191014221427-bfbb95714c53 6 | -------------------------------------------------------------------------------- /opa-auth/go.sum: -------------------------------------------------------------------------------- 1 | github.com/adaptant-labs/go-function-sdk v0.0.0-20191014221427-bfbb95714c53 h1:L9KoeA5AlJ3KVLsWfYNKpEVPyMZELfyTzarlv/3WvXo= 2 | github.com/adaptant-labs/go-function-sdk v0.0.0-20191014221427-bfbb95714c53/go.mod h1:TzQDRDQPIiRyJUDkkdGoC54+iEQyscU6uUYFJ6TWG9Y= 3 | -------------------------------------------------------------------------------- /opa-auth/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Adaptant Solutions AG 2019. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | package function 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "io/ioutil" 10 | "log" 11 | "net" 12 | "net/http" 13 | "os" 14 | 15 | "github.com/adaptant-labs/go-function-sdk" 16 | ) 17 | 18 | var ( 19 | opaServer string 20 | ) 21 | 22 | type opaResponse struct { 23 | Result map[string]bool `json:"result"` 24 | } 25 | 26 | func checkAuthz(values map[string]map[string]interface{}) (opaResponse, error) { 27 | jsonValue, err := json.Marshal(values) 28 | if err != nil { 29 | log.Println("Failed marshalling OPA payload", err) 30 | return opaResponse{}, err 31 | } 32 | 33 | opaUri := opaServer + "/v1/data/openfaas/authz" 34 | 35 | resp, err := http.Post(opaUri, "application/json", bytes.NewBuffer(jsonValue)) 36 | if err != nil { 37 | log.Println("Failed delivering OPA payload", err) 38 | return opaResponse{}, err 39 | } 40 | 41 | body, err := ioutil.ReadAll(resp.Body) 42 | if err != nil { 43 | log.Println("Failed reading response body", err) 44 | return opaResponse{}, err 45 | } 46 | 47 | var response opaResponse 48 | 49 | err = json.Unmarshal(body, &response) 50 | if err != nil { 51 | log.Println("Failed unmarshalling response body", err) 52 | return opaResponse{}, err 53 | } 54 | 55 | return response, nil 56 | } 57 | 58 | func isAuthorized(req handler.Request) bool { 59 | host, port, err := net.SplitHostPort(req.Host) 60 | if err != nil { 61 | return false 62 | } 63 | 64 | authzRequestPayload := map[string]map[string]interface{}{ 65 | "input": { 66 | "function": host, 67 | "port": port, 68 | "method": req.Method, 69 | "query": req.QueryString, 70 | "user": req.Header.Get("Authorization"), 71 | }, 72 | } 73 | 74 | // Check for a decision from OPA - if none is available, default-deny 75 | response, err := checkAuthz(authzRequestPayload) 76 | if err != nil { 77 | return false 78 | } 79 | 80 | // Otherwise default-allow 81 | allow := true 82 | 83 | // Iterate over decision results looking for any deny cases 84 | for k, v := range response.Result { 85 | if k == "deny" && v == true { 86 | allow = false 87 | } else if k == "allow" && v == false { 88 | allow = false 89 | } 90 | } 91 | 92 | return allow 93 | } 94 | 95 | func init() { 96 | opaServer = os.Getenv("OPA_URL") 97 | if opaServer == "" { 98 | opaServer = "http://localhost:8181" 99 | } 100 | } 101 | 102 | // Handle a function invocation 103 | func Handle(req handler.Request) (handler.Response, error) { 104 | var err error 105 | res := handler.Response{} 106 | 107 | if !isAuthorized(req) { 108 | message := "Unauthorized." 109 | res.Body = []byte(message) 110 | res.StatusCode = http.StatusUnauthorized 111 | return res, err 112 | } 113 | 114 | res.StatusCode = http.StatusOK 115 | res.Body = []byte("Authorization OK.") 116 | 117 | return res, err 118 | } 119 | -------------------------------------------------------------------------------- /opa-auth/policy/example.rego: -------------------------------------------------------------------------------- 1 | package openfaas.authz 2 | 3 | default allow = false 4 | 5 | allow { 6 | input.function == "opa-auth" 7 | input.user == "alice" 8 | } 9 | -------------------------------------------------------------------------------- /stack.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: faas 3 | gateway: http://127.0.0.1:8080 4 | network: func_functions 5 | 6 | functions: 7 | opa-auth: 8 | lang: golang-http-gomod 9 | handler: ./opa-auth 10 | image: adaptant/openfaas-function-auth-opa:0.1 11 | environment: 12 | OPA_URL: "http://opa:8181" 13 | --------------------------------------------------------------------------------