From cfb03ca495111cbcce98acce93bf96b8433f4f35 Mon Sep 17 00:00:00 2001 From: Emil Volckmar Ry Date: Sat, 8 Jul 2023 22:53:20 +0200 Subject: [PATCH 1/7] introduce new builtin crypto.parse_private_keys * crypto.parse_private_keys parses private keys, returns a list of valid keys * consolidated getPrivateKeysFromString with getPrivateKeysFromPEMData for crypto.x509.parse_rsa_private_key and crypto.parse_private_keys * reworked crypto.x509.parse_rsa_private_key so that we no longer need getPrivateKeysFromString but instead determine type of key in builtin function and added input validation checks Co-authored-by: Charlie Egan Signed-off-by: Emil Volckmar Ry --- ast/builtins.go | 14 + builtin_metadata.json | 21 + capabilities.json | 25 ++ .../test-cryptoparsersaprivatekey-1.yaml | 35 ++ topdown/crypto.go | 156 ++++++-- topdown/crypto_test.go | 362 +++++++++++++----- 6 files changed, 484 insertions(+), 129 deletions(-) create mode 100644 test/cases/testdata/cryptoparsersaprivatekeys/test-cryptoparsersaprivatekey-1.yaml diff --git a/ast/builtins.go b/ast/builtins.go index fa882da627d..63ba033658d 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -211,6 +211,7 @@ var DefaultBuiltins = [...]*Builtin{ CryptoX509ParseCertificateRequest, CryptoX509ParseRSAPrivateKey, CryptoX509ParseKeyPair, + CryptoParsePrivateKeys, CryptoHmacMd5, CryptoHmacSha1, CryptoHmacSha256, @@ -2312,6 +2313,19 @@ var CryptoX509ParseRSAPrivateKey = &Builtin{ ), } +var CryptoParsePrivateKeys = &Builtin{ + Name: "crypto.parse_private_keys", + Description: `Returns zero or more private keys from the given encoded string containing DER certificate data. + +If the input is empty, the function will return null. The input string should be a list of one or more concatenated PEM blocks. The whole input of concatenated PEM blocks can optionally be Base64 encoded.`, + Decl: types.NewFunction( + types.Args( + types.Named("keys", types.S).Description("PEM encoded data containing one or more private keys as concatenated blocks. Optionally Base64 encoded."), + ), + types.Named("output", types.NewArray(nil, types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)))).Description("parsed private keys represented as objects"), + ), +} + var CryptoMd5 = &Builtin{ Name: "crypto.md5", Description: "Returns a string representing the input string hashed with the MD5 function", diff --git a/builtin_metadata.json b/builtin_metadata.json index 0cf978f2014..22a0d5654c3 100644 --- a/builtin_metadata.json +++ b/builtin_metadata.json @@ -39,6 +39,7 @@ "crypto.hmac.sha256", "crypto.hmac.sha512", "crypto.md5", + "crypto.parse_private_keys", "crypto.sha1", "crypto.sha256", "crypto.x509.parse_and_verify_certificates", @@ -3712,6 +3713,26 @@ }, "wasm": false }, + "crypto.parse_private_keys": { + "args": [ + { + "description": "PEM encoded data containing one or more private keys as concatenated blocks. Optionally Base64 encoded.", + "name": "keys", + "type": "string" + } + ], + "available": [ + "edge" + ], + "description": "Returns zero or more private keys from the given encoded string containing DER certificate data.\n\nIf the input is empty, the function will return null. The input string should be a list of one or more concatenated PEM blocks. The whole input of concatenated PEM blocks can optionally be Base64 encoded.", + "introduced": "edge", + "result": { + "description": "parsed private keys represented as objects", + "name": "output", + "type": "array[object[string: any]]" + }, + "wasm": false + }, "crypto.sha1": { "args": [ { diff --git a/capabilities.json b/capabilities.json index a89d1f859e8..1b0b80322be 100644 --- a/capabilities.json +++ b/capabilities.json @@ -671,6 +671,31 @@ "type": "function" } }, + { + "name": "crypto.parse_private_keys", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, { "name": "crypto.sha1", "decl": { diff --git a/test/cases/testdata/cryptoparsersaprivatekeys/test-cryptoparsersaprivatekey-1.yaml b/test/cases/testdata/cryptoparsersaprivatekeys/test-cryptoparsersaprivatekey-1.yaml new file mode 100644 index 00000000000..1ceb5f580fb --- /dev/null +++ b/test/cases/testdata/cryptoparsersaprivatekeys/test-cryptoparsersaprivatekey-1.yaml @@ -0,0 +1,35 @@ +cases: + - data: + modules: + - | + package testing + + pem := "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA9D/bK4171aiTNUkrUCHKGMLSQooV+o3wdz2889h9iv0HhhBJ\nCAGU54K3duB8ofHpmYL50QodcR4RLw1vSkaI+FPdPDMyKxKj/YcmofJjz4kW+Iqw\nFbBcbMnKnEVzye+CyW9YYOTu0xWtcgen80zGp2opG0GZX86hBjjXJnjOdrJTk6x2\nNAiJIbjsQevysmj+2MyqVm8widxw0x+rGhTaCD+ZXWitN0a0WO1aaA8c/7i99I9z\nhe2peKvXzEtMaqYO9ptHcYmq2z0QWvZuJVMv5Yn0mScLWyh91R099IOtn6sNaMMs\nOyTpi7E/2IlVgI2uKGPEopKkMFV8Fl2YaAbo7wIDAQABAoIBAAyMZ08ygqU0dvOq\n4a3JPp/NCo5el8h6mFsX8eg5PCHy4/sQRSBDLIpEXfaei+iqDA1V/E2wDlksaUeY\nkhony4uui1Q3cSFjYMd6tRJm6JfV/DcisO88U1NHfsBOlSdPxdFhhhHcUSTJHVMZ\nb5iBXkdlnd0HnsCcVguCyhLw6/KPFyiA+NYRz68flxze7admyVp5C6i/HbMPq8Pr\nMilBUvOFtxuaGeJBAiavuzUe9I70dRwpe424tMvisSA8h7Xbm8BeN/PJHDV/2JrI\nURgQ563yQ5So/Qg8AgxXRkpgWM9zAh7r31PBO86vq/B4ZbON/TtWdcZVsAcVB4Pk\ntqc8JNkCgYEA+g8V+y92SETdcwUkbd5O9Fg5CkfdsALsBXVH6FunrCUV5HS9l5o6\nMMBbJ/08odW/bP5BmOa4A/Hbk9uG/UfQn2KQ3HCgPlxUEwQO07R1/FcQOe4xmyG6\nJpDgQ30viE1RtlCkceQWUeitCIqZsYu0i8sLZLWJH+V/07OB4G17ELMCgYEA+g1v\nhrlAFNhZvrIX/zcP3xF2pZ+AqkFXdL/tWQZkWAVToONn/LlXTH71C/TO2x+OaQRm\nqX1bA9Zhyjf1gYQN9RenjUswvggk0aY2Tk28wUqowMGSsjQHmZ20EphHNMWNJpdS\nfKFfrQIFKCnLlpQVNz+j3bLWZUnq+jPaYnJP7NUCgYEA48qcVo7c7Ga3aNEVZ3St\nbg90HrZq760pvqshDz13V+0MrWnfUFxxh/mi0KHy+uYRlMNllFkQ5p8LTP0dUlt6\nY8dReU6r20MWX6BBtX9eP7o8ENm4nL4zqnAtq609gKgWuMNrmkiSQJl6Dx7bdY5z\nsSkNPvfUa5cQRBTxSjXRdtsCgYBHrzpdwRXh4/Q2ew/uFnbyWCtPZ96W8IyF58+/\nSdnSchR7dzYEeY3RXEQb3V6/6tgEu0JDLLC+9OKr+kbjjlwB+3oJQ5kBoYwMnj3L\nTPXj4+dk+xl3BPt4yoEpI4amVkwU2CTJnemzy3R3AyReUq2SXSg5El/sQbifaeYd\neu/20QKBgH/5IZHGBKiRAe1ww2FzOpDtL8VXXTe3EAXKutfajrHTqPz9+lXknX/D\nUMosh264nYXYS29WqxhJVutbE9u8e0VpuY1qIN9/3R0WKfTLTMUFlZtbqTepvsy1\nW2UbK732I4Nfp0/mtUvOSdMZO8dxbSdEeMnw/Ec8QgxK9a1rRu9+\n-----END RSA PRIVATE KEY-----" + + p { + count(crypto.parse_private_keys(pem)) == 1 + } + note: cryptoparseprivatekey/valid + query: data.testing.p = x + want_result: + - x: true + - data: + modules: + - | + package testing + pem := "nope" + p := crypto.parse_private_keys(pem) + note: cryptoparseprivatekey/invalid + query: data.testing.p = x + want_result: + - x: [] + - data: + modules: + - | + package testing + pem := "" + p := crypto.parse_private_keys(pem) + note: cryptoparseprivatekey/invalid + query: data.testing.p = x + want_result: + - x: null \ No newline at end of file diff --git a/topdown/crypto.go b/topdown/crypto.go index 5c4cf1b9736..c33abb2c93c 100644 --- a/topdown/crypto.go +++ b/topdown/crypto.go @@ -6,6 +6,7 @@ package topdown import ( "bytes" + "crypto" "crypto/hmac" "crypto/md5" "crypto/sha1" @@ -21,8 +22,9 @@ import ( "os" "strings" - "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/internal/jwx/jwk" + + "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/topdown/builtins" "github.com/open-policy-agent/opa/util" ) @@ -39,7 +41,8 @@ const ( blockTypeRSAPrivateKey = "RSA PRIVATE KEY" // blockTypeRSAPrivateKey indicates this PEM block contains a RSA private key. // Exported for tests. - blockTypePrivateKey = "PRIVATE KEY" + blockTypePrivateKey = "PRIVATE KEY" + blockTypeEcPrivateKey = "EC PRIVATE KEY" ) func builtinCryptoX509ParseCertificates(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { @@ -168,7 +171,8 @@ func builtinCryptoX509ParseCertificateRequest(_ BuiltinContext, operands []*ast. return iter(ast.NewTerm(v)) } -func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { +func builtinCryptoJWKFromPrivateKey(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + var x interface{} a := operands[0].Value input, err := builtins.StringOperand(a, 1) @@ -177,22 +181,42 @@ func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, operands []*ast.Term, } // get the raw private key - rawKey, err := getRSAPrivateKeyFromString(string(input)) + pemDataString := string(input) + + if pemDataString == "" { + return fmt.Errorf("input PEM data was empty") + } + + // This built in must be supplied a valid PEM or base64 encoded string. + // If the input is not a PEM string, attempt to decode b64. + // If the base64 decode fails - this is an error + if !strings.HasPrefix(pemDataString, "-----BEGIN") { + bs, err := base64.StdEncoding.DecodeString(pemDataString) + if err != nil { + return err + } + pemDataString = string(bs) + } + + rawKeys, err := getPrivateKeysFromPEMData(pemDataString) if err != nil { return err } - rsaPrivateKey, err := jwk.New(rawKey) + if len(rawKeys) == 0 { + return iter(ast.NullTerm()) + } + + key, err := jwk.New(rawKeys[0]) if err != nil { return err } - jsonKey, err := json.Marshal(rsaPrivateKey) + jsonKey, err := json.Marshal(key) if err != nil { return err } - var x interface{} if err := util.UnmarshalJSON(jsonKey, &x); err != nil { return err } @@ -205,6 +229,46 @@ func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, operands []*ast.Term, return iter(ast.NewTerm(value)) } +func builtinCryptoParsePrivateKeys(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + + a := operands[0].Value + input, err := builtins.StringOperand(a, 1) + if err != nil { + return err + } + + if string(input) == "" { + return iter(ast.NullTerm()) + } + + // get the raw private key + rawKeys, err := getPrivateKeysFromPEMData(string(input)) + if err != nil { + return err + } + + if len(rawKeys) == 0 { + return iter(ast.NewTerm(ast.NewArray())) + } + + bs, err := json.Marshal(rawKeys) + if err != nil { + return err + } + + var x interface{} + if err := util.UnmarshalJSON(bs, &x); err != nil { + return err + } + + value, err := ast.InterfaceToValue(x) + if err != nil { + return err + } + + return iter(ast.NewTerm(value)) +} + func hashHelper(a ast.Value, h func(ast.String) string) (ast.Value, error) { s, err := builtins.StringOperand(a, 1) if err != nil { @@ -298,7 +362,8 @@ func init() { RegisterBuiltinFunc(ast.CryptoSha1.Name, builtinCryptoSha1) RegisterBuiltinFunc(ast.CryptoSha256.Name, builtinCryptoSha256) RegisterBuiltinFunc(ast.CryptoX509ParseCertificateRequest.Name, builtinCryptoX509ParseCertificateRequest) - RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoX509ParseRSAPrivateKey) + RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoJWKFromPrivateKey) + RegisterBuiltinFunc(ast.CryptoParsePrivateKeys.Name, builtinCryptoParsePrivateKeys) RegisterBuiltinFunc(ast.CryptoX509ParseKeyPair.Name, builtinCryptoX509ParseKeyPair) RegisterBuiltinFunc(ast.CryptoHmacMd5.Name, builtinCryptoHmacMd5) RegisterBuiltinFunc(ast.CryptoHmacSha1.Name, builtinCryptoHmacSha1) @@ -378,43 +443,56 @@ func getX509CertsFromPem(pemBlocks []byte) ([]*x509.Certificate, error) { return x509.ParseCertificates(decodedCerts) } -func getRSAPrivateKeyFromString(key string) (interface{}, error) { - // if the input is PEM handle that - if strings.HasPrefix(key, "-----BEGIN") { - return getRSAPrivateKeyFromPEM([]byte(key)) - } +func getPrivateKeysFromPEMData(pemData string) ([]crypto.PrivateKey, error) { + pemBlockString := pemData - // assume input is base64 if not PEM - b64, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return nil, err - } - - return getRSAPrivateKeyFromPEM(b64) -} - -func getRSAPrivateKeyFromPEM(pemBlocks []byte) (interface{}, error) { - - // decode the pem into the Block struct - p, _ := pem.Decode(pemBlocks) - if p == nil { - return nil, fmt.Errorf("failed to parse PEM block containing the key") - } + var validPrivateKeys []crypto.PrivateKey - // if the key is in PKCS1 format - if p.Type == blockTypeRSAPrivateKey { - return x509.ParsePKCS1PrivateKey(p.Bytes) + // if the input is base64, decode it + bs, err := base64.StdEncoding.DecodeString(pemBlockString) + if err == nil { + pemBlockString = string(bs) } + bs = []byte(pemBlockString) - // if the key is in PKCS8 format - if p.Type == blockTypePrivateKey { - return x509.ParsePKCS8PrivateKey(p.Bytes) - } + for len(bs) > 0 { + inputLen := len(bs) + var block *pem.Block + block, bs = pem.Decode(bs) + if block == nil && len(bs) == 0 { + break + } + // should only happen if end of input is not a valid PEM block. See TestParseRSAPrivateKeyVariedPemInput. + if inputLen == len(bs) { + break + } - // unsupported key format - return nil, fmt.Errorf("PEM block type is '%s', expected %s or %s", p.Type, blockTypeRSAPrivateKey, - blockTypePrivateKey) + if block == nil { + continue + } + switch block.Type { + case blockTypeRSAPrivateKey: + parsedKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + validPrivateKeys = append(validPrivateKeys, parsedKey) + case blockTypePrivateKey: + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + validPrivateKeys = append(validPrivateKeys, parsedKey) + case blockTypeEcPrivateKey: + parsedKey, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + validPrivateKeys = append(validPrivateKeys, parsedKey) + } + } + return validPrivateKeys, nil } // addCACertsFromFile adds CA certificates from filePath into the given pool. diff --git a/topdown/crypto_test.go b/topdown/crypto_test.go index bdfa49048e9..e70476df7ec 100644 --- a/topdown/crypto_test.go +++ b/topdown/crypto_test.go @@ -1,15 +1,17 @@ package topdown import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "encoding/base64" + "fmt" "strings" "testing" - - "github.com/open-policy-agent/opa/internal/jwx/jwk" ) -func TestX509ParseAndVerify(t *testing.T) { - rootCA := `-----BEGIN CERTIFICATE----- +var rootCA = `-----BEGIN CERTIFICATE----- MIIBoDCCAUagAwIBAgIRAJXcMYZALXooNq/VV/grXhMwCgYIKoZIzj0EAwIwLjER MA8GA1UEChMIT1BBIFRlc3QxGTAXBgNVBAMTEE9QQSBUZXN0IFJvb3QgQ0EwHhcN MjEwNzAxMTc0MTUzWhcNMzEwNjI5MTc0MTUzWjAuMREwDwYDVQQKEwhPUEEgVGVz @@ -20,7 +22,7 @@ HRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBTch60qxQvLl+AfDfcaXmjvT8GvpzAK BggqhkjOPQQDAgNIADBFAiBqraIP0l2U0oNuH0+rf36hDks94wSB5EGlGH3lYNMR ugIhANkbukX5hOP8pJDRWP/pYuv6MBnRY4BS8gpp9Vu31qOb -----END CERTIFICATE-----` - intermediateCA := `-----BEGIN CERTIFICATE----- +var intermediateCA = `-----BEGIN CERTIFICATE----- MIIByDCCAW6gAwIBAgIQC0k4DPGrh9me73EJX5zntTAKBggqhkjOPQQDAjAuMREw DwYDVQQKEwhPUEEgVGVzdDEZMBcGA1UEAxMQT1BBIFRlc3QgUm9vdCBDQTAeFw0y MTA3MDExNzQxNTNaFw0zMTA2MjkxNzQxNTNaMDYxETAPBgNVBAoTCE9QQSBUZXN0 @@ -32,7 +34,7 @@ ALy/9WEwHwYDVR0jBBgwFoAU3IetKsULy5fgHw33Gl5o70/Br6cwCgYIKoZIzj0E AwIDSAAwRQIgUwsYApW9Tsm6AstWswaKGie0srB4FUkUbfKwWmUI2JgCIQCBTySN MF+EiQAMKyz/N9KUuXEckC356WvKcyJaYYcV0w== -----END CERTIFICATE-----` - leaf := `-----BEGIN CERTIFICATE----- +var leaf = `-----BEGIN CERTIFICATE----- MIIB8zCCAZqgAwIBAgIRAID4gPKg7DDiuOfzUYFSXLAwCgYIKoZIzj0EAwIwNjER MA8GA1UEChMIT1BBIFRlc3QxITAfBgNVBAMTGE9QQSBUZXN0IEludGVybWVkaWF0 ZSBDQTAeFw0yMTA3MDUxNzQ5NTBaFw0zNjA3MDExNzQ5NDdaMCUxIzAhBgNVBAMT @@ -46,73 +48,7 @@ ADBEAiAtmZewL94ijN0YwUGaJM9BXCaoTQPwkzugqjCj+K912QIgKKFvbPu4asrE nwy7dzejHmQUcZ/aUNbc4VTbiv15ESk= -----END CERTIFICATE-----` - t.Run("TestFullChainPEM", func(t *testing.T) { - chain := strings.Join([]string{rootCA, intermediateCA, leaf}, "\n") - - parsed, err := getX509CertsFromString(chain) - if err != nil { - t.Fatalf("failed to parse PEM cert chain: %v", err) - } - - if _, err := verifyX509CertificateChain(parsed); err != nil { - t.Error("x509 verification failed when it was expected to succeed") - } - }) - - t.Run("TestFullChainBase64", func(t *testing.T) { - chain := strings.Join([]string{rootCA, intermediateCA, leaf}, "\n") - b64 := base64.StdEncoding.EncodeToString([]byte(chain)) - - parsed, err := getX509CertsFromString(b64) - if err != nil { - t.Fatalf("failed to parse base64 cert chain: %v", err) - } - - if _, err := verifyX509CertificateChain(parsed); err != nil { - t.Error("x509 verification failed when it was expected to succeed") - } - }) - - t.Run("TestWrongOrder", func(t *testing.T) { - chain := strings.Join([]string{leaf, intermediateCA, rootCA}, "\n") - - parsed, err := getX509CertsFromString(chain) - if err != nil { - t.Fatalf("failed to parse PEM cert chain: %v", err) - } - - if _, err := verifyX509CertificateChain(parsed); err == nil { - t.Error("x509 verification succeeded when it was expected to fail") - } - }) - - t.Run("TestMissingIntermediate", func(t *testing.T) { - chain := strings.Join([]string{rootCA, leaf}, "\n") - - parsed, err := getX509CertsFromString(chain) - if err != nil { - t.Fatalf("failed to parse PEM cert chain: %v", err) - } - - if _, err := verifyX509CertificateChain(parsed); err == nil { - t.Error("x509 verification succeeded when it was expected to fail") - } - }) - - t.Run("TestTooFewCerts", func(t *testing.T) { - parsed, err := getX509CertsFromString(leaf) - if err != nil { - t.Fatalf("failed to parse leaf cert: %v", err) - } - - if _, err := verifyX509CertificateChain(parsed); err == nil { - t.Error("x509 verification succeeded when it was expected to fail") - } - }) -} - -func TestParseRSAPrivateKey(t *testing.T) { - rsaPrivateKey := `-----BEGIN RSA PRIVATE KEY----- +var rsaPrivateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA3Y8cXdK06ufUSP035jiwJk8IsuwGjJD/LSRvE2AhJL/Vp9mu 41z1bV5Mi/TTK/uZNqv6VdvTxFPZOUYycLXEchg8L6wrOLgAX0DleP+YTKGG4oyg dTZZcqzwr4p7WhYzLFmpW8RCLgHJbV0fF1pejJKtV+9fpsdX8oQzKvqO39ne1hl+ @@ -140,7 +76,7 @@ GjJuyhAhz5VdHn2H2+RptQ70RVM+ctDNKYZko2aH4uGZq/6X5MWGr1erLMgMbg5q +oSLpOUiUobapGdl9fgHetyFw/N9TI1tl/4+2uFqW5knBQnXByPP -----END RSA PRIVATE KEY-----` - rsaPrivateKeyPKCS8 := `-----BEGIN PRIVATE KEY----- +var rsaPrivateKeyPKCS8 = `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDP7abKDTHtqGkk 6c/jxbZph17QcVz3NxcRrQ8RCLWHZd020oANIssGgZwGuy9hvQUfEYRy1+78wJmV c7naeJ8qkLj1u0OsDLwofRaYXzkZUFitZr2Ygkzhy8/GVhdIMVnAV2u4LHvpw+dS @@ -169,41 +105,106 @@ DFa6BZS0N0x374JRidFWV0a+Mz7pTqC0TO/M3+y6yaDd766J3bkdh2sq8pnhAnXc qPYXB5U6tdTrexzaYBKr4gQ= -----END PRIVATE KEY-----` - t.Run("TestParseRSAPrivateKey", func(t *testing.T) { - parsed, err := getRSAPrivateKeyFromString(rsaPrivateKey) +var keyPemEC = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49 +AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q +EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA== +-----END EC PRIVATE KEY-----` + +var keyEd25519 = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJHG93jlLLLTF6Stky5+8Q7mMpgCkYYTO12NDAzlJn3w +-----END PRIVATE KEY----- +` + +var partiallyValidPEMString = ` +something else +-----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIJHG93jlLLLTF6Stky5+8Q7mMpgCkYYTO12NDAzlJn3w +-----END PRIVATE KEY----- +something else +-----BEGIN CERTIFICATE----- +MIIBcDCCARagAwIBAgIJAMZmuGSIfvgzMAoGCCqGSM49BAMCMBMxETAPBgNVBAMM +CHdoYXRldmVyMB4XDTE4MDgxMDE0Mjg1NFoXDTE4MDkwOTE0Mjg1NFowEzERMA8G +A1UEAwwId2hhdGV2ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATPwn3WCEXL +mjp/bFniDwuwsfu7bASlPae2PyWhqGeWwe23Xlyx+tSqxlkXYe4pZ23BkAAscpGj +yn5gXHExyDlKo1MwUTAdBgNVHQ4EFgQUElRjSoVgKjUqY5AXz2o74cLzzS8wHwYD +VR0jBBgwFoAUElRjSoVgKjUqY5AXz2o74cLzzS8wDwYDVR0TAQH/BAUwAwEB/zAK +BggqhkjOPQQDAgNIADBFAiEA4yQ/88ZrUX68c6kOe9G11u8NUaUzd8pLOtkKhniN +OHoCIHmNX37JOqTcTzGn2u9+c8NlnvZ0uDvsd1BmKPaUmjmm +-----END CERTIFICATE----- +something else +-----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIJHG93jlLLLTF6Stky5+8Q7mMpgCkYYTO12NDAzlJn3w +-----END PRIVATE KEY----- +something else +` +var invalidData = `nothingtoseehere` + +func TestX509ParseAndVerify(t *testing.T) { + + t.Run("TestFullChainPEM", func(t *testing.T) { + chain := strings.Join([]string{rootCA, intermediateCA, leaf}, "\n") + + parsed, err := getX509CertsFromString(chain) if err != nil { - t.Fatalf("failed to parse PEM cert: %v", err) + t.Fatalf("failed to parse PEM cert chain: %v", err) } - if _, err := jwk.New(parsed); err != nil { - t.Errorf("RSA private key failed when it was expected to succeed, got %v", err) + if _, err := verifyX509CertificateChain(parsed); err != nil { + t.Error("x509 verification failed when it was expected to succeed") } }) - t.Run("TestParseRSAPrivateKeyBase64", func(t *testing.T) { - b64 := base64.StdEncoding.EncodeToString([]byte(rsaPrivateKey)) + t.Run("TestFullChainBase64", func(t *testing.T) { + chain := strings.Join([]string{rootCA, intermediateCA, leaf}, "\n") + b64 := base64.StdEncoding.EncodeToString([]byte(chain)) - parsed, err := getRSAPrivateKeyFromString(b64) + parsed, err := getX509CertsFromString(b64) if err != nil { - t.Fatalf("failed to parse PEM cert: %v", err) + t.Fatalf("failed to parse base64 cert chain: %v", err) } - if _, err := jwk.New(parsed); err != nil { - t.Errorf("RSA private key (base64) failed when it was expected to succeed, got %v", err) + if _, err := verifyX509CertificateChain(parsed); err != nil { + t.Error("x509 verification failed when it was expected to succeed") } }) - t.Run("TestParseRSAPrivateKeyPKCS8", func(t *testing.T) { - parsed, err := getRSAPrivateKeyFromString(rsaPrivateKeyPKCS8) + t.Run("TestWrongOrder", func(t *testing.T) { + chain := strings.Join([]string{leaf, intermediateCA, rootCA}, "\n") + + parsed, err := getX509CertsFromString(chain) if err != nil { - t.Fatalf("failed to parse PEM cert: %v", err) + t.Fatalf("failed to parse PEM cert chain: %v", err) } - if _, err := jwk.New(parsed); err != nil { - t.Errorf("RSA private key (PKCS8) failed when it was expected to succeed, got %v", err) + if _, err := verifyX509CertificateChain(parsed); err == nil { + t.Error("x509 verification succeeded when it was expected to fail") } }) + t.Run("TestMissingIntermediate", func(t *testing.T) { + chain := strings.Join([]string{rootCA, leaf}, "\n") + + parsed, err := getX509CertsFromString(chain) + if err != nil { + t.Fatalf("failed to parse PEM cert chain: %v", err) + } + + if _, err := verifyX509CertificateChain(parsed); err == nil { + t.Error("x509 verification succeeded when it was expected to fail") + } + }) + + t.Run("TestTooFewCerts", func(t *testing.T) { + parsed, err := getX509CertsFromString(leaf) + if err != nil { + t.Fatalf("failed to parse leaf cert: %v", err) + } + + if _, err := verifyX509CertificateChain(parsed); err == nil { + t.Error("x509 verification succeeded when it was expected to fail") + } + }) } func Test_parsex509KeyPair(t *testing.T) { @@ -465,3 +466,184 @@ KcZjiyUsFLvdC5de1MeT1rJjQEsiZxH+QPR88tuByUVG000lpA== }) } + +func Test_getPrivateKeyFromPEMData(t *testing.T) { + tests := map[string]struct { + input string + wantErr string + keyCheck func(t *testing.T, keys []crypto.PrivateKey) + }{ + "invalid data": { + input: invalidData, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 0 { + t.Fatalf("expected no keys but got %d", len(keys)) + } + }, + }, + "rsa key": { + input: rsaPrivateKey, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "base64 rsa key": { + input: base64.StdEncoding.EncodeToString([]byte(rsaPrivateKey)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "rsa key pkcs8": { + input: rsaPrivateKeyPKCS8, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "base64 rsa key pkcs8": { + input: base64.StdEncoding.EncodeToString([]byte(rsaPrivateKeyPKCS8)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "ec key": { + input: keyPemEC, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + }, + }, + "base64 ec key": { + input: base64.StdEncoding.EncodeToString([]byte(keyPemEC)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + }, + }, + "ed key": { + input: keyEd25519, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + }, + }, + "base64 ed key": { + input: base64.StdEncoding.EncodeToString([]byte(keyEd25519)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + }, + }, + "other PEM data, no keys": { + input: fmt.Sprintf("%s\n%s\n%s\n", rootCA, intermediateCA, leaf), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 0 { + t.Fatalf("expected no keys but got %d", len(keys)) + } + }, + }, + "partially valid PEM data": { + input: partiallyValidPEMString, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 2 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + if _, ok := keys[1].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + }, + }, + "mixed PEM data": { + input: fmt.Sprintf("%s\n%s\n%s\n%s", rootCA, intermediateCA, leaf, keyPemEC), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + }, + }, + "mixed PEM data, two keys": { + input: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", rootCA, intermediateCA, leaf, keyPemEC, rsaPrivateKey), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 2 { + t.Fatalf("expected 2 keys but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + if _, ok := keys[1].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "corrupted key": { + input: `-----BEGIN PRIVATE KEY----- +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +-----END PRIVATE KEY----- +`, + wantErr: "asn1: structure error", + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 0 { + t.Fatalf("expected no keys but got %d", len(keys)) + } + }, + }, + } + for name, testData := range tests { + t.Run(name, func(t *testing.T) { + keys, err := getPrivateKeysFromPEMData(testData.input) + if testData.wantErr != "" { + if err != nil && !strings.Contains(err.Error(), testData.wantErr) { + t.Fatalf("got error: %v, want error: %v", err, testData.wantErr) + } else if err == nil { + t.Fatalf("expected error: %v", testData.wantErr) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + testData.keyCheck(t, keys) + }) + } +} From afcf01457c5ce8a0138a57636bc691c5850ce3d9 Mon Sep 17 00:00:00 2001 From: Ashutosh Narkar Date: Fri, 14 Jul 2023 00:47:50 -0700 Subject: [PATCH 2/7] topdown: Honor default keyword on functions Default functions satisfy the following properties: * Same arity as other functions with the same name * Arguments should only be plain variables ie. no composite values. For ex, default f([x]) = 1 is an invalid default function * Variable names should not be repeated ie. default f(x, x) = 1 is an invalid default function Fixes: #2445 Signed-off-by: Ashutosh Narkar --- ast/compile_test.go | 35 ++++++ ast/parser.go | 38 ++++++ ast/parser_test.go | 8 ++ internal/wasm/sdk/test/e2e/exceptions.yaml | 1 - .../test-default-functions.yaml | 34 +++--- .../functions/test-functions-default.yaml | 112 +++++++++++++++--- topdown/eval.go | 17 ++- 7 files changed, 209 insertions(+), 36 deletions(-) diff --git a/ast/compile_test.go b/ast/compile_test.go index 06890bf7a18..8340a515b75 100644 --- a/ast/compile_test.go +++ b/ast/compile_test.go @@ -1844,6 +1844,41 @@ bar.baz contains "quz" if true`, assertCompilerErrorStrings(t, c, expected) } +func TestCompilerCheckRuleConflictsDefaultFunction(t *testing.T) { + tests := []struct { + note string + modules []*Module + err string + }{ + { + note: "conflicting rules", + modules: modules( + `package pkg + default f(_) = 100 + f(x, y) = x { + x == y + }`), + err: "rego_type_error: conflicting rules data.pkg.f found", + }, + } + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + mods := make(map[string]*Module, len(tc.modules)) + for i, m := range tc.modules { + mods[fmt.Sprint(i)] = m + } + c := NewCompiler() + c.Modules = mods + compileStages(c, c.checkRuleConflicts) + if tc.err != "" { + assertCompilerErrorStrings(t, c, []string{tc.err}) + } else { + assertCompilerErrorStrings(t, c, []string{}) + } + }) + } +} + func TestCompilerCheckRuleConflictsDotsInRuleHeads(t *testing.T) { tests := []struct { diff --git a/ast/parser.go b/ast/parser.go index 58e9e73c8a6..3337a964e45 100644 --- a/ast/parser.go +++ b/ast/parser.go @@ -614,6 +614,12 @@ func (p *Parser) parseRules() []*Rule { return nil } + if len(rule.Head.Args) > 0 { + if !p.validateDefaultRuleArgs(&rule) { + return nil + } + } + rule.Body = NewBody(NewExpr(BooleanTerm(true).SetLocation(rule.Location)).SetLocation(rule.Location)) return []*Rule{&rule} } @@ -2176,6 +2182,38 @@ func (p *Parser) validateDefaultRuleValue(rule *Rule) bool { return valid } +func (p *Parser) validateDefaultRuleArgs(rule *Rule) bool { + + valid := true + vars := NewVarSet() + + vis := NewGenericVisitor(func(x interface{}) bool { + switch x := x.(type) { + case Var: + if vars.Contains(x) { + p.error(rule.Loc(), fmt.Sprintf("illegal default rule (arguments cannot be repeated %v)", x)) + valid = false + return true + } + vars.Add(x) + + case *Term: + switch v := x.Value.(type) { + case Var: // do nothing + default: + p.error(rule.Loc(), fmt.Sprintf("illegal default rule (arguments cannot contain %v)", TypeName(v))) + valid = false + return true + } + } + + return false + }) + + vis.Walk(rule.Head.Args) + return valid +} + // We explicitly use yaml unmarshalling, to accommodate for the '_' in 'related_resources', // which isn't handled properly by json for some reason. type rawAnnotation struct { diff --git a/ast/parser_test.go b/ast/parser_test.go index 5c8422cc7d1..11d522fd9c4 100644 --- a/ast/parser_test.go +++ b/ast/parser_test.go @@ -1611,6 +1611,14 @@ func TestRule(t *testing.T) { assertParseErrorContains(t, "default invalid rule head builtin call", `default a = upper("foo")`, "illegal default rule (value cannot contain call)") assertParseErrorContains(t, "default invalid rule head call", `default a = b`, "illegal default rule (value cannot contain var)") + assertParseErrorContains(t, "default invalid function head ref", `default f(x) = b.c.d`, "illegal default rule (value cannot contain ref)") + assertParseErrorContains(t, "default invalid function head call", `default f(x) = g(x)`, "illegal default rule (value cannot contain call)") + assertParseErrorContains(t, "default invalid function head builtin call", `default f(x) = upper("foo")`, "illegal default rule (value cannot contain call)") + assertParseErrorContains(t, "default invalid function head call", `default f(x) = b`, "illegal default rule (value cannot contain var)") + assertParseErrorContains(t, "default invalid function composite argument", `default f([x]) = 1`, "illegal default rule (arguments cannot contain array)") + assertParseErrorContains(t, "default invalid function number argument", `default f(1) = 1`, "illegal default rule (arguments cannot contain number)") + assertParseErrorContains(t, "default invalid function repeated vars", `default f(x, x) = 1`, "illegal default rule (arguments cannot be repeated x)") + assertParseError(t, "extra braces", `{ a := 1 }`) assertParseError(t, "invalid rule name hyphen", `a-b = x { x := 1 }`) diff --git a/internal/wasm/sdk/test/e2e/exceptions.yaml b/internal/wasm/sdk/test/e2e/exceptions.yaml index 714af1ba93b..664e29b20f7 100644 --- a/internal/wasm/sdk/test/e2e/exceptions.yaml +++ b/internal/wasm/sdk/test/e2e/exceptions.yaml @@ -1,5 +1,4 @@ # Exception Format is : -"functions/default": "not supported in topdown, https://github.com/open-policy-agent/opa/issues/2445" "data/toplevel integer": "https://github.com/open-policy-agent/opa/issues/3711" "data/nested integer": "https://github.com/open-policy-agent/opa/issues/3711" "withkeyword/function: indirect call, arity 1, replacement is value that needs eval (array comprehension)": "https://github.com/open-policy-agent/opa/issues/5311" diff --git a/test/cases/testdata/defaultkeyword/test-default-functions.yaml b/test/cases/testdata/defaultkeyword/test-default-functions.yaml index 6ece1772036..81350089c9f 100644 --- a/test/cases/testdata/defaultkeyword/test-default-functions.yaml +++ b/test/cases/testdata/defaultkeyword/test-default-functions.yaml @@ -1,5 +1,3 @@ -# NOTE(sr): default functions are not supported, but they had sneaked into -# the full extent of a package. These cases assert that this won't happen. cases: - note: defaultkeyword/function with var arg modules: @@ -10,24 +8,32 @@ cases: query: data.test = x want_result: - x: {} -- note: defaultkeyword/function with ground arg +- note: defaultkeyword/function with var arg, ref head modules: - | package test - default f(10) := 100 - query: data.test = x - want_result: - - x: {} -- note: defaultkeyword/function with ground arg, ref head - modules: - - | - package test - - default p.q.r.f(10) := 100 + default p.q.r.f(x) := 100 query: data.test = x want_result: - x: p: q: - r: {} \ No newline at end of file + r: {} +- note: defaultkeyword/function with var arg, ref head query + modules: + - | + package test + + default p.q.r.f(x) := 100 + + p.q.r.f(x) = x { + x == 2 + } + + foo { + p.q.r.f(3) == 100 + } + query: data.test.foo = x + want_result: + - x: true \ No newline at end of file diff --git a/test/cases/testdata/functions/test-functions-default.yaml b/test/cases/testdata/functions/test-functions-default.yaml index 00c7abcd291..6706ac4d18e 100644 --- a/test/cases/testdata/functions/test-functions-default.yaml +++ b/test/cases/testdata/functions/test-functions-default.yaml @@ -1,21 +1,99 @@ cases: - data: modules: - - | - package p.m - - default hello = false - - hello() = m { - m = input.message - 1 == 2 - m = "world" - } - h = m { - m = hello() - } - note: functions/default # not supported but shouldn't panic - query: data.p.m = x + - | + package test + + default f(x) = 1 + + f(x) = x { + x > 0 + } + + p { + f(-1) == 1 + } + + note: functions/default + query: data.test.p = x + want_result: + - x: true + +- data: + modules: + - | + package test + + default f(x) = 1 + + f(x) = x { + x > 0 + } + + p { + f(2) == 2 + } + + note: functions/non default + query: data.test.p = x + want_result: + - x: true + +- data: + modules: + - | + package test + + default f(x) = 1 + + p { + f(2) == 1 + } + + note: functions/only default + query: data.test.p = x + want_result: + - x: true + +- data: + modules: + - | + package test + + default f(_, _) = 1 + + f(x, y) = x { + x == y + } + + p { + f(2, 2) == 2 + } + + note: functions/wildcard args + query: data.test.p = x + want_result: + - x: true + +- data: + modules: + - | + package test + + default f(x) = 1000 + + f(x) = x { + x > 0 + } + + p = xs { + xs := [y | x = [1, -2, 3][_]; y := f(x)] + } + + note: functions/comprehensions + query: data.test.p = x want_result: - - x: - hello: false + - x: + - 1 + - 1000 + - 3 diff --git a/topdown/eval.go b/topdown/eval.go index 545dcff8fe7..49cf0e5dcf6 100644 --- a/topdown/eval.go +++ b/topdown/eval.go @@ -1795,13 +1795,16 @@ type evalFunc struct { func (e evalFunc) eval(iter unifyIterator) error { - // default functions aren't supported: - // https://github.com/open-policy-agent/opa/issues/2445 - if len(e.ir.Rules) == 0 { + if e.ir.Empty() { return nil } - argCount := len(e.ir.Rules[0].Head.Args) + var argCount int + if len(e.ir.Rules) > 0 { + argCount = len(e.ir.Rules[0].Head.Args) + } else if e.ir.Default != nil { + argCount = len(e.ir.Default.Head.Args) + } if len(e.ir.Else) > 0 && e.e.unknown(e.e.query[e.e.index], e.e.bindings) { // Partial evaluation of ordered rules is not supported currently. Save the @@ -1820,6 +1823,7 @@ func (e evalFunc) eval(iter unifyIterator) error { return e.partialEvalSupport(argCount, iter) } } + return suppressEarlyExit(e.evalValue(iter, argCount, e.ir.EarlyExit)) } @@ -1859,6 +1863,11 @@ func (e evalFunc) evalValue(iter unifyIterator, argCount int, findOne bool) erro } } + if e.ir.Default != nil && prev == nil { + _, err := e.evalOneRule(iter, e.ir.Default, cacheKey, prev, findOne) + return err + } + return nil } From a9961889368f877fbb78e8d7e09679154e2fc13f Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 20 Jul 2023 09:53:33 +0100 Subject: [PATCH 3/7] Update the standalone envoy tutorial (#6105) The tutorial now uses kind as well as updated versions for envoy. I have made some adjustments to how the bundle is served and the test commands run to exercise the policy too. Signed-off-by: Charlie Egan --- .../envoy-tutorial-standalone-envoy.md | 653 +++++++++++------- 1 file changed, 398 insertions(+), 255 deletions(-) diff --git a/docs/content/envoy-tutorial-standalone-envoy.md b/docs/content/envoy-tutorial-standalone-envoy.md index f03db6d6cfa..6b27ca55c5c 100644 --- a/docs/content/envoy-tutorial-standalone-envoy.md +++ b/docs/content/envoy-tutorial-standalone-envoy.md @@ -4,33 +4,250 @@ kind: envoy weight: 10 --- -The tutorial shows how Envoy’s External authorization filter can be used with -OPA as an authorization service to enforce security policies over API requests +The tutorial shows how Envoy’s External +[authorization filter](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ext_authz_filter.html) +can be used with OPA as an authorization service to enforce security policies over API requests received by Envoy. The tutorial also covers examples of authoring custom policies over the HTTP request body. -## Prerequisites +## Overview -This tutorial requires Kubernetes 1.20 or later. To run the tutorial locally, we -recommend using [minikube](https://minikube.sigs.k8s.io/docs/start/) in -version `v1.21+` with Kubernetes 1.20+. +In this tutorial we'll see how to use OPA as an External +Authorization service for the Envoy proxy. We'll do this by: -## Steps +* Running a local Kubernetes cluster +* Creating a simple authorization policy in Rego and serving it via the Bundle API +* Deploying a sample application with Envoy and OPA sidecars +* Run some sample requests to see the policy in action -### 1. Start Minikube +Note that other than the HTTP client and bundle server, all components +are co-located in the same pod. -```bash -minikube start +## Running a local Kubernetes cluster + +To start a local Kubernetes cluster to run our demo, we'll be using +[kind](https://kind.sigs.k8s.io/). In order to use the `kind` command, +you'll need to have Docker installed on your machine. Running +`docker info` is the easiest way to check if Docker is installed and +running. + +You should see output simil + +```shell +$ docker info +Client: + ... + +Server: + ... +``` + +If the above command shows information for both the client and server, +then Docker is installed and running. + +{{< info >}} +If you haven't used `kind` before, you can find installation instructions +in the [project documentation](https://kind.sigs.k8s.io/#installation-and-usage). +{{}} + +Create a cluster with the following command: + +```shell +$ kind create cluster --name opa-envoy --image kindest/node:v1.27.3 +Creating cluster "opa-envoy" ... + ✓ Ensuring node image (kindest/node:v1.27.3) 🖼 + ✓ Preparing nodes 📦 + ✓ Writing configuration 📜 + ✓ Starting control-plane 🕹️ + ✓ Installing CNI 🔌 + ✓ Installing StorageClass 💾 +... +``` + +Once the cluster is created, make sure your `kubectl` context is set to connect +to the new cluster: + +```shell +$ kubectl cluster-info --context kind-opa-envoy +Kubernetes control plane is running at ... +CoreDNS is running at ... +... +``` + +Listing the cluster nodes, should show something like this: + +```shell +$ kubectl get nodes +NAME STATUS ROLES AGE VERSION +opa-envoy-control-plane Ready control-plane 2m35s v1.27.3 +``` + +## Creating & Serving our Policy Bundle + +This tutorial assumes you have some Rego knowledge, in summary the policy below does the following: + +* Checks that the JWT token is valid +* Checks that the action is allowed based on the token payload `role` and the request path +* Guests have read-only access to the `/people` endpoint, admins can create users too as long as the + name is not the same as the admin's name. + +```rego +# policy.rego +package envoy.authz + +import future.keywords.if + +import input.attributes.request.http as http_request + +default allow := false + +allow if { + is_token_valid + action_allowed +} + +is_token_valid if { + token.valid + now := time.now_ns() / 1000000000 + token.payload.nbf <= now + now < token.payload.exp +} + +action_allowed if { + http_request.method == "GET" + token.payload.role == "guest" + glob.match("/people", ["/"], http_request.path) +} + +action_allowed if { + http_request.method == "GET" + token.payload.role == "admin" + glob.match("/people", ["/"], http_request.path) +} + +action_allowed if { + http_request.method == "POST" + token.payload.role == "admin" + glob.match("/people", ["/"], http_request.path) + lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub) +} + +token := {"valid": valid, "payload": payload} if { + [_, encoded] := split(http_request.headers.authorization, " ") + [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) +} +``` + +Create a file called `policy.rego` with the above content and store it in a ConfigMap: + +```shell +kubectl create configmap authz-policy --from-file policy.rego ``` -### 2. Create ConfigMap containing configuration for Envoy +Now that the policy is stored in a ConfigMap, we can spin up an HTTP server to make it +available to as a Bundle to OPA when it's making decisions for our application: + +```yaml +# bundle-server.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bundle-server + labels: + app: bundle-server +spec: + replicas: 1 + selector: + matchLabels: + app: bundle-server + template: + metadata: + labels: + app: bundle-server + spec: + initContainers: + - name: opa-builder + image: openpolicyagent/opa:latest + args: + - "build" + - "--bundle" + - "/opt/policy/" + - "--output" + - "/opt/output/bundle.tar.gz" + volumeMounts: + - name: index + mountPath: /opt/output/ + - name: policy + mountPath: /opt/policy/ + containers: + - name: bundle-server + image: nginx:1.25 + ports: + - containerPort: 80 + name: http + volumeMounts: + - name: index + mountPath: /usr/share/nginx/html + volumes: + - name: index + emptyDir: {} + - name: policy + configMap: + name: authz-policy +--- +apiVersion: v1 +kind: Service +metadata: + name: bundle-server +spec: + selector: + app: bundle-server + ports: + - protocol: TCP + port: 80 + targetPort: http +``` + +Create a file called `bundle-server.yaml` with the above content and apply it to the cluster: + +```shell +kubectl apply -f bundle-server.yaml +``` + +Once the deployment is running, we can check that the bundle is available by running: + +```shell +kubectl port-forward service/bundle-server 8080:80 +``` + +Before checking that the bundle has been generated correctly and is available to download: + +```shell +$ curl -I localhost:8080/bundle.tar.gz +HTTP/1.1 200 OK +... +``` + +You may now exit the port-forwarding session, the bundle server will only be accessed +from inside the cluster from now on. + +## Deploying an application with Envoy and OPA sidecars + +In this tutorial, we are manually configuring the Envoy proxy sidecar to intermediate +HTTP traffic from clients and our application. Envoy will consult OPA to +make authorization decisions for each request by sending `CheckRequest` messages over +a gRPC connection. -The Envoy configuration below defines an external authorization filter -`envoy.ext_authz` for a gRPC authorization server. +We will use the following Envoy configuration to achieve this. In summary, this +configures Envoy to: -Save the configuration as **envoy.yaml**: +* Listen on port `8000` for HTTP traffic +* Consult OPA for authorization decisions at 127.0.0.1:9191 & deny failing requests +* Forward requests to the application at 127.0.0.1:8080 if ok. ```yaml +# envoy.yaml static_resources: listeners: - address: @@ -70,6 +287,8 @@ static_resources: stat_prefix: ext_authz timeout: 0.5s - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: service connect_timeout: 0.25s @@ -103,134 +322,25 @@ layered_runtime: global_downstream_max_connections: 50000 ``` -Create the ConfigMap: - -```bash -kubectl create configmap proxy-config --from-file envoy.yaml -``` - -### 3. Define a OPA policy - -The following OPA policy restricts access to the `/people` endpoint exposed by -our sample app: - -* Alice is granted a **guest** role and can perform a `GET` request to `/people`. -* Bob is granted an **admin** role and can perform a `GET` and `POST` request to `/people`. - -The policy also restricts an `admin` user, in this case `bob` from creating an -employee with the same `firstname` as himself. - -**policy.rego** - -```live:example:module:openable -package envoy.authz - -import future.keywords - -import input.attributes.request.http as http_request - -default allow := false - -allow if { - is_token_valid - action_allowed -} - -is_token_valid if { - token.valid - now := time.now_ns() / 1000000000 - token.payload.nbf <= now - now < token.payload.exp -} - -action_allowed if { - http_request.method == "GET" - token.payload.role == "guest" - glob.match("/people", ["/"], http_request.path) -} - -action_allowed if { - http_request.method == "GET" - token.payload.role == "admin" - glob.match("/people", ["/"], http_request.path) -} - -action_allowed if { - http_request.method == "POST" - token.payload.role == "admin" - glob.match("/people", ["/"], http_request.path) - lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub) -} - -token := {"valid": valid, "payload": payload} if { - [_, encoded] := split(http_request.headers.authorization, " ") - [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) -} -``` - -Then, build an OPA bundle. +Create a `ConfigMap` containing the above configuration by running: ```shell -opa build policy.rego -``` - -In the next step, OPA is configured to query for the `data.envoy.authz.allow` -decision. If the response is `true` the operation is allowed, otherwise the -operation is denied. Sample input received by OPA is shown below: - -```live:example:query:hidden -data.envoy.authz.allow -``` - -```live:example:input -{ - "attributes": { - "request": { - "http": { - "method": "GET", - "path": "/people", - "headers": { - "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU" - } - } - } - } -} -``` - -With the input value above, the answer is: - -```live:example:output -``` - -An example of the complete input received by OPA can be seen [here](https://github.com/open-policy-agent/opa-envoy-plugin#example-input). - -### 4. Publish OPA Bundle - -We will now serve the OPA bundle created in the previous step using Nginx. - -```bash -docker run --rm --name bundle-server -d -p 8888:80 -v ${PWD}:/usr/share/nginx/html:ro nginx:latest +kubectl create configmap proxy-config --from-file envoy.yaml ``` -The above command will start a Nginx server running on port `8888` on your host and act as a bundle server. - -### 5. Create App Deployment with OPA and Envoy sidecars - -Our deployment contains a sample Go app which provides information about -employees in a company. It exposes a `/people` endpoint to `get` and `create` -employees. More information can on the app be found -[here](https://github.com/ashutosh-narkar/go-test-server). - -OPA is started with a configuration that sets the listening address of Envoy -External Authorization gRPC server and specifies the name of the policy decision -to query. OPA will also periodically download the policy bundle from the local Nginx server -configured in the previous step. More information on the configuration options can be found -[here](https://github.com/open-policy-agent/opa-envoy-plugin#configuration). - -Save the deployment as **deployment.yaml**: +Our application will be configured using a `Deployment` and `Service`. +There are a few things to note: +* the pods have an `initContainer` that configures the `iptables` rules to + redirect traffic to the Envoy proxy. +* the `demo-test-server` container is a simple user store using in-memory state. +* the `envoy` container is configured to use the `proxy-config` `ConfigMap` we + created earlier. +* The OPA container is configured to download policy bundles from + the in-cluster bundle server (`bundle-server.default.svc.cluster.local`). +* The OPA license key must be set. We show how to do this in the next step. ```yaml +# app.yaml kind: Deployment apiVersion: apps/v1 metadata: @@ -251,172 +361,205 @@ spec: - name: proxy-init image: openpolicyagent/proxy_init:v8 # Configure the iptables bootstrap script to redirect traffic to the - # Envoy proxy on port 8000, specify that Envoy will be running as user - # 1111, and that we want to exclude port 8282 from the proxy for the - # OPA health checks. These values must match up with the configuration - # defined below for the "envoy" and "opa" containers. + # Envoy proxy on port 8000. Envoy will be running as 1111, and port + # 8282 will be excluded to support OPA health checks. args: ["-p", "8000", "-u", "1111", "-w", "8282"] securityContext: capabilities: add: - - NET_ADMIN + - NET_ADMIN runAsNonRoot: false runAsUser: 0 containers: - - name: app - image: openpolicyagent/demo-test-server:v1 - ports: - - containerPort: 8080 - - name: envoy - image: envoyproxy/envoy:v1.20.0 - volumeMounts: - - readOnly: true - mountPath: /config - name: proxy-config - args: - - "envoy" - - "--config-path" - - "/config/envoy.yaml" - env: - - name: ENVOY_UID - value: "1111" - - name: opa - # Note: openpolicyagent/opa:latest-envoy is created by retagging - # the latest released image of OPA-Envoy. - image: openpolicyagent/opa:{{< current_opa_envoy_docker_version >}} - args: - - "run" - - "--server" - - "--addr=localhost:8181" - - "--diagnostic-addr=0.0.0.0:8282" - - "--set=services.default.url=http://host.minikube.internal:8888" - - "--set=bundles.default.resource=bundle.tar.gz" - - "--set=plugins.envoy_ext_authz_grpc.addr=:9191" - - "--set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow" - - "--set=decision_logs.console=true" - - "--set=status.console=true" - - "--ignore=.*" - livenessProbe: - httpGet: - path: /health?plugins - scheme: HTTP - port: 8282 - initialDelaySeconds: 5 - periodSeconds: 5 - readinessProbe: - httpGet: - path: /health?plugins - scheme: HTTP - port: 8282 - initialDelaySeconds: 5 - periodSeconds: 5 + - name: app + image: openpolicyagent/demo-test-server:v1 + ports: + - containerPort: 8080 + - name: envoy + image: envoyproxy/envoy:v1.26.3 + volumeMounts: + - readOnly: true + mountPath: /config + name: proxy-config + args: + - "envoy" + - "--config-path" + - "/config/envoy.yaml" + env: + - name: ENVOY_UID + value: "1111" + - name: opa + image: openpolicyagent/opa:latest-envoy + args: + - "run" + - "--server" + - "--addr=localhost:8181" + - "--diagnostic-addr=0.0.0.0:8282" + - "--set=services.default.url=http://bundle-server" + - "--set=bundles.default.resource=bundle.tar.gz" + - "--set=plugins.envoy_ext_authz_grpc.addr=:9191" + - "--set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow" + - "--set=decision_logs.console=true" + - "--set=status.console=true" + - "--ignore=.*" + livenessProbe: + httpGet: + path: /health?plugins + scheme: HTTP + port: 8282 + initialDelaySeconds: 5 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /health?plugins + scheme: HTTP + port: 8282 + initialDelaySeconds: 1 + periodSeconds: 3 volumes: - - name: proxy-config - configMap: - name: proxy-config -``` - -```bash -kubectl apply -f deployment.yaml + - name: proxy-config + configMap: + name: proxy-config +--- +apiVersion: v1 +kind: Service +metadata: + name: example-app +spec: + selector: + app: example-app + ports: + - protocol: TCP + port: 80 + targetPort: 8080 ``` -Check that the Pod shows `3/3` containers `READY` the `STATUS` as `Running`: - -```bash -kubectl get pod +Deploy the application and Kubernetes Service to the cluster with: -NAME READY STATUS RESTARTS AGE -example-app-67c644b9cb-bbqgh 3/3 Running 0 8s +```shell +kubectl apply -f app.yaml ``` -> The `proxy-init` container installs iptables rules to redirect all container - traffic through the Envoy proxy sidecar. More information can be found - [here](https://github.com/open-policy-agent/contrib/tree/main/envoy_iptables). +Check that everything is working by listing the pod (make sure that +all three pods are running ok). +```shell +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +bundle-server-5d7bfffdb6-bgn86 1/1 Running 0 1m +example-app-74b4bc88-5d4wh 3/3 Running 0 1m +``` -### 6. Create a Service to expose HTTP server +## See the Policy in Action -In a second terminal, start a [minikube tunnel](https://minikube.sigs.k8s.io/docs/handbook/accessing/#using-minikube-tunnel) to allow for use of the `LoadBalancer` service type. +Run a shell inside the cluster to use for testing. We will use this in-cluster +shell for the rest of the tutorial. -```bash -minikube tunnel +```shell +kubectl run curl --restart=Never -it --rm --image curlimages/curl:8.1.2 -- sh ``` -In the first terminal, create a `LoadBalancer` service for the deployment. +Set two tokens for two users, Alice and Bob with different permissions. +As defined by our policy: -```bash -kubectl expose deployment example-app --type=LoadBalancer --name=example-app-service --port=8080 +```shell +export ALICE_TOKEN="eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJleHAiOiAyMjQxMDgxNTM5LCAibmJmIjogMTUxNDg1MTEzOSwgInJvbGUiOiAiZ3Vlc3QiLCAic3ViIjogIllXeHBZMlU9In0.Uk5hgUqMuUfDLvBLnlXMD0-X53aM_Hlziqg3vhOsCc8" +export BOB_TOKEN="eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJleHAiOiAyMjQxMDgxNTM5LCAibmJmIjogMTUxNDg1MTEzOSwgInJvbGUiOiAiYWRtaW4iLCAic3ViIjogIlltOWkifQ.5qsm7rRTvqFHAgiB6evX0a_hWnGbWquZC0HImVQPQo8" ``` -Check that the Service shows an `EXTERNAL-IP`: +### Listing People -```bash -kubectl get service example-app-service +Send a request to list people. This should succeed for both Alice and Bob. -NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -example-app-service LoadBalancer 10.109.64.199 10.109.64.199 8080:32170/TCP 5s +```shell +curl -i -H "Authorization: Bearer $ALICE_TOKEN" http://example-app/people ``` - -Set the `SERVICE_URL` environment variable to the service's IP/port. - -**minikube:** - -```bash -export SERVICE_HOST=$(kubectl get service example-app-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}') -export SERVICE_URL=$SERVICE_HOST:8080 -echo $SERVICE_URL +``` +HTTP/1.1 200 OK +content-type: application/json +date: Tue, 18 Jul 2023 15:22:25 GMT +content-length: 96 +x-envoy-upstream-service-time: 14 +server: envoy + +[{"id":"1","firstname":"John","lastname":"Doe"},{"id":"2","firstname":"Jane","lastname":"Doe"}] ``` -**minikube (example):** +And for Bob: +```shell +curl -i -H "Authorization: Bearer $BOB_TOKEN" http://example-app/people +``` ``` -10.109.64.199:8080 +HTTP/1.1 200 OK +...omitted... ``` -### 7. Exercise the OPA policy +### Creating People -For convenience, we’ll want to store Alice's and Bob's tokens in environment variables. +Send a request to create a new user. This should fail for Alice but not Bob: -```bash -export ALICE_TOKEN="eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJleHAiOiAyMjQxMDgxNTM5LCAibmJmIjogMTUxNDg1MTEzOSwgInJvbGUiOiAiZ3Vlc3QiLCAic3ViIjogIllXeHBZMlU9In0.Uk5hgUqMuUfDLvBLnlXMD0-X53aM_Hlziqg3vhOsCc8" -export BOB_TOKEN="eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJleHAiOiAyMjQxMDgxNTM5LCAibmJmIjogMTUxNDg1MTEzOSwgInJvbGUiOiAiYWRtaW4iLCAic3ViIjogIlltOWkifQ.5qsm7rRTvqFHAgiB6evX0a_hWnGbWquZC0HImVQPQo8" +```shell +curl -i -H "Authorization: Bearer $ALICE_TOKEN" \ + -d '{"firstname":"Foo", "lastname":"Bar"}' -H "Content-Type: application/json" \ + -X POST http://example-app/people ``` +``` +HTTP/1.1 403 Forbidden +date: Tue, 18 Jul 2023 15:25:28 GMT +server: envoy +content-length: 0 +``` +And for Bob, the request is permitted and the user is saved with an ID -Check that `Alice` can get employees **but cannot** create one. - -```bash -curl -i -H "Authorization: Bearer $ALICE_TOKEN" http://$SERVICE_URL/people -curl -i -H "Authorization: Bearer $ALICE_TOKEN" -d '{"firstname":"Charlie", "lastname":"OPA"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people +```shell +curl -i -H "Authorization: Bearer $BOB_TOKEN" \ + -d '{"firstname":"Foo", "lastname":"Bar"}' -H "Content-Type: application/json" \ + -X POST http://example-app/people +``` ``` +HTTP/1.1 200 OK +content-type: application/json +date: Tue, 18 Jul 2023 15:28:20 GMT +content-length: 51 +x-envoy-upstream-service-time: 11 +server: envoy + +{"id":"498081","firstname":"Foo","lastname":"Bar"} +``` + +### Creating People: Conflict -Check that `Bob` can get employees and also create one. +Our policy also blocks users from creating users with the same name, test that +functionality with this request: -```bash -curl -i -H "Authorization: Bearer $BOB_TOKEN" http://$SERVICE_URL/people -curl -i -H "Authorization: Bearer $BOB_TOKEN" -d '{"firstname":"Charlie", "lastname":"Opa"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people +```shell +curl -i -H "Authorization: Bearer $BOB_TOKEN" \ + -d '{"firstname":"Bob", "lastname":"Bar"}' -H "Content-Type: application/json" \ + -X POST http://example-app/people +``` +``` +HTTP/1.1 403 Forbidden +date: Tue, 18 Jul 2023 15:31:48 GMT +server: envoy +content-length: 0 ``` -Check that `Bob` **cannot** create an employee with the same firstname as himself. +## Shutting Down -```bash -curl -i -H "Authorization: Bearer $BOB_TOKEN" -d '{"firstname":"Bob", "lastname":"Rego"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people -``` +Exit the in-cluster shell by typing `exit`. -To remove the kubernetes resources created during this tutorial please use the following commands. -```bash -kubectl delete service example-app-service -kubectl delete deployment example-app -kubectl delete configmap proxy-config -``` +Delete the cluster by running: -To remove the bundle server run: -```bash -docker rm -f bundle-server +```shell +$ kind delete cluster --name opa-envoy +Deleting cluster "opa-envoy" ... +Deleted nodes: ["opa-envoy-control-plane"] ``` ## Wrap Up -Congratulations for finishing the tutorial ! +Congratulations on finishing the tutorial ! This tutorial showed how to use OPA as an External authorization service to enforce custom policies by leveraging Envoy’s External authorization filter. From 93c5abe7ba36199dcb5ed76cc95b9fab98cfad8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:17:09 +0200 Subject: [PATCH 4/7] build(deps): bump go.uber.org/automaxprocs from 1.5.2 to 1.5.3 (#6106) Bumps [go.uber.org/automaxprocs](https://github.com/uber-go/automaxprocs) from 1.5.2 to 1.5.3. - [Release notes](https://github.com/uber-go/automaxprocs/releases) - [Changelog](https://github.com/uber-go/automaxprocs/blob/master/CHANGELOG.md) - [Commits](https://github.com/uber-go/automaxprocs/compare/v1.5.2...v1.5.3) --- updated-dependencies: - dependency-name: go.uber.org/automaxprocs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups.go | 4 ++-- vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups2.go | 4 ++++ .../go.uber.org/automaxprocs/internal/cgroups/mountpoint.go | 4 ++++ vendor/modules.txt | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e2bea9bad10..bb2f13bfa72 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 go.opentelemetry.io/otel/sdk v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 - go.uber.org/automaxprocs v1.5.2 + go.uber.org/automaxprocs v1.5.3 golang.org/x/net v0.12.0 golang.org/x/time v0.3.0 google.golang.org/grpc v1.56.2 diff --git a/go.sum b/go.sum index 0d69cbf50c7..223366a6dea 100644 --- a/go.sum +++ b/go.sum @@ -336,8 +336,8 @@ go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLk go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME= -go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups.go b/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups.go index e6c17319790..e89f5436028 100644 --- a/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups.go +++ b/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups.go @@ -110,8 +110,8 @@ func (cg CGroups) CPUQuota() (float64, bool, error) { } cfsPeriodUs, err := cpuCGroup.readInt(_cgroupCPUCFSPeriodUsParam) - if err != nil { - return -1, false, err + if defined := cfsPeriodUs > 0; err != nil || !defined { + return -1, defined, err } return float64(cfsQuotaUs) / float64(cfsPeriodUs), true, nil diff --git a/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups2.go b/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups2.go index 3ac10c8b6fc..78556062fe2 100644 --- a/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups2.go +++ b/vendor/go.uber.org/automaxprocs/internal/cgroups/cgroups2.go @@ -159,6 +159,10 @@ func (cg *CGroups2) CPUQuota() (float64, bool, error) { if err != nil { return -1, false, err } + + if period == 0 { + return -1, false, errors.New("zero value for period is not allowed") + } } return float64(max) / float64(period), true, nil diff --git a/vendor/go.uber.org/automaxprocs/internal/cgroups/mountpoint.go b/vendor/go.uber.org/automaxprocs/internal/cgroups/mountpoint.go index 2efde4c4ba9..f3877f78aa6 100644 --- a/vendor/go.uber.org/automaxprocs/internal/cgroups/mountpoint.go +++ b/vendor/go.uber.org/automaxprocs/internal/cgroups/mountpoint.go @@ -95,8 +95,12 @@ func NewMountPointFromLine(line string) (*MountPoint, error) { for i, field := range fields[_miFieldIDOptionalFields:] { if field == _mountInfoOptionalFieldsSep { + // End of optional fields. fsTypeStart := _miFieldIDOptionalFields + i + 1 + // Now we know where the optional fields end, split the line again with a + // limit to avoid issues with spaces in super options as present on WSL. + fields = strings.SplitN(line, _mountInfoSep, fsTypeStart+_miFieldCountSecondHalf) if len(fields) != fsTypeStart+_miFieldCountSecondHalf { return nil, mountPointFormatInvalidError{line} } diff --git a/vendor/modules.txt b/vendor/modules.txt index f8825ba2939..29cc2f5e007 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -319,7 +319,7 @@ go.opentelemetry.io/proto/otlp/collector/trace/v1 go.opentelemetry.io/proto/otlp/common/v1 go.opentelemetry.io/proto/otlp/resource/v1 go.opentelemetry.io/proto/otlp/trace/v1 -# go.uber.org/automaxprocs v1.5.2 +# go.uber.org/automaxprocs v1.5.3 ## explicit; go 1.18 go.uber.org/automaxprocs/internal/cgroups go.uber.org/automaxprocs/internal/runtime From e57b6c748c68fbb45e9827cdfe39be42464e6234 Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 20 Jul 2023 17:47:57 +0100 Subject: [PATCH 5/7] [docs] Correct envoy tutorial mistake (#6107) This line appears to have been truncated. Signed-off-by: Charlie Egan --- docs/content/envoy-tutorial-standalone-envoy.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/content/envoy-tutorial-standalone-envoy.md b/docs/content/envoy-tutorial-standalone-envoy.md index 6b27ca55c5c..21fa44a8868 100644 --- a/docs/content/envoy-tutorial-standalone-envoy.md +++ b/docs/content/envoy-tutorial-standalone-envoy.md @@ -31,7 +31,8 @@ you'll need to have Docker installed on your machine. Running `docker info` is the easiest way to check if Docker is installed and running. -You should see output simil +You should see output similar to the following, showing information about +the Docker client **and** server on our machine: ```shell $ docker info From c83f5b78baa1a7601c56889e25a8701cf25c5528 Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 20 Jul 2023 17:39:33 +0100 Subject: [PATCH 6/7] [docs] Fix bug for broken ecosystem links Reproduce Bug: * Visit: https://www.openpolicyagent.org/docs/edge/policy-language/#ecosystem-projects * Click `view on the OPA ecosystem page` I have also tried to use the titles from items in links when the titles are short enough. Signed-off-by: Charlie Egan --- docs/website/layouts/docs/ecosystem-single.html.html | 3 ++- .../partials/ecosystem-project-list-for-feature.html | 9 +++++++-- .../layouts/shortcodes/ecosystem_feature_embed.html | 5 ++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/website/layouts/docs/ecosystem-single.html.html b/docs/website/layouts/docs/ecosystem-single.html.html index 2fda50399e8..fcadb8c2739 100644 --- a/docs/website/layouts/docs/ecosystem-single.html.html +++ b/docs/website/layouts/docs/ecosystem-single.html.html @@ -22,7 +22,8 @@ {{ if (gt (len $selectedIntegrations) 0) }}

Integrations are ordered by the amount of linked content.

{{ $sortedIntegrations := partial "functions/sort-integrations" (dict "integrations" $selectedIntegrations) }} - {{ partial "ecosystem-project-list-for-feature" (dict "selectedIntegrations" $sortedIntegrations "integrationsData" $.Site.Data.integrations "feature" $feature) }} + {{ $base := delimit (first 3 (split .Page.RelPermalink "/")) "/" }} + {{ partial "ecosystem-project-list-for-feature" (dict "selectedIntegrations" $sortedIntegrations "integrationsData" $.Site.Data.integrations "feature" $feature "base" $base) }} {{ else }}

There are no integrations for this category.

{{ end }} diff --git a/docs/website/layouts/partials/ecosystem-project-list-for-feature.html b/docs/website/layouts/partials/ecosystem-project-list-for-feature.html index c634abff717..9876c7a3fcb 100644 --- a/docs/website/layouts/partials/ecosystem-project-list-for-feature.html +++ b/docs/website/layouts/partials/ecosystem-project-list-for-feature.html @@ -1,6 +1,7 @@ {{ $selectedIntegrations := (index . "selectedIntegrations") }} {{ $integrationsData := (index . "integrationsData") }} {{ $feature := (index . "feature") }} +{{ $base := (index . "base") }}
{{ range $name := $selectedIntegrations }} @@ -34,8 +35,12 @@
{{ (index (index $integration.docs_features $feature) "note") | markdownify }}
- - View on the OPA Ecosystem page + + {{ if lt (len $integration.title) 30 }} + View {{ $integration.title }} in Ecosystem + {{ else }} + View in Ecosystem + {{ end }}
{{ end }} diff --git a/docs/website/layouts/shortcodes/ecosystem_feature_embed.html b/docs/website/layouts/shortcodes/ecosystem_feature_embed.html index 679596376fb..aeeca371308 100644 --- a/docs/website/layouts/shortcodes/ecosystem_feature_embed.html +++ b/docs/website/layouts/shortcodes/ecosystem_feature_embed.html @@ -1,8 +1,8 @@ {{ $feature := .Get "key" }} {{ $selectedIntegrations := partial "functions/select-integrations-by-docs-feature" (dict "integrations" $.Site.Data.integrations.integrations "feature" $feature) }} +{{ $base := delimit (first 3 (split .Page.RelPermalink "/")) "/" }} {{ if (gt (len $selectedIntegrations) 6) }} - {{ $base := delimit (first 3 (split .Page.RelPermalink "/")) "/" }}

The {{ len $selectedIntegrations }} ecosystem projects related to this page can be found in the corresponding OPA Ecosystem section. @@ -14,9 +14,8 @@

{{ $sortedIntegrations := partial "functions/sort-integrations" (dict "integrations" $selectedIntegrations) }} - {{ partial "ecosystem-project-list-for-feature" (dict "selectedIntegrations" $sortedIntegrations "integrationsData" $.Site.Data.integrations "feature" $feature) }} + {{ partial "ecosystem-project-list-for-feature" (dict "selectedIntegrations" $sortedIntegrations "integrationsData" $.Site.Data.integrations "feature" $feature "base" $base) }} - {{ $base := delimit (first 3 (split .Page.RelPermalink "/")) "/" }}

View these projects in the OPA Ecosystem.

From 768dcd9b4c6d6499e3b746a9b0527504785b5ace Mon Sep 17 00:00:00 2001 From: Ashutosh Narkar Date: Thu, 20 Jul 2023 16:03:27 -0700 Subject: [PATCH 7/7] docs: Add a note about default functions Signed-off-by: Ashutosh Narkar --- docs/content/policy-language.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/content/policy-language.md b/docs/content/policy-language.md index e94dcf83a29..e99948f6561 100644 --- a/docs/content/policy-language.md +++ b/docs/content/policy-language.md @@ -1960,6 +1960,27 @@ a variable or reference. If the value is a composite then it may not contain variables or references. Comprehensions however may, as the result of a comprehension is never undefined. +Similar to rules, the `default` keyword can be applied to functions as well. + +For example: + +```live:eg/defaultfunc:module:read_only +default clamp_positive(x) := 0 + +clamp_positive(x) = x { + x > 0 +} +``` + +When `clamp_positive` is queried, the return value will be either the argument provided to the function or `0`. + +The value of a `default` function follows the same conditions as that of a `default` rule. In addition, a `default` +function satisfies the following properties: + +* same arity as other functions with the same name +* arguments should only be plain variables ie. no composite values +* argument names should not be repeated + ## Else Keyword The ``else`` keyword is a basic control flow construct that gives you control