Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate Authpolicy from OpenAPI 3.0.X #46

Merged
merged 9 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/generate_kuadrant.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func generateKuadrantCommand() *cobra.Command {
}

cmd.AddCommand(generateKuadrantRateLimitPolicyCommand())
cmd.AddCommand(generateKuadrantAuthPolicyCommand())

return cmd
}
97 changes: 97 additions & 0 deletions cmd/generate_kuadrant_authpolicy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package cmd

import (
"encoding/json"
"fmt"

"github.com/getkin/kin-openapi/openapi3"
kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2"
"github.com/spf13/cobra"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/kuadrant/kuadrantctl/pkg/gatewayapi"
"github.com/kuadrant/kuadrantctl/pkg/kuadrantapi"
"github.com/kuadrant/kuadrantctl/pkg/utils"
)

//kuadrantctl generate kuadrant authpolicy --oas [OAS_FILE_PATH | OAS_URL | @]

func generateKuadrantAuthPolicyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "authpolicy",
Short: "Generate Kuadrant AuthPolicy from OpenAPI 3.0.X",
Long: "Generate Kuadrant AuthPolicy from OpenAPI 3.0.X",
RunE: runGenerateKuadrantAuthPolicy,
}

// OpenAPI ref
cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteOAS, "oas", "", "/path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required)")
err := cmd.MarkFlagRequired("oas")
if err != nil {
panic(err)
}

return cmd
}

func runGenerateKuadrantAuthPolicy(cmd *cobra.Command, args []string) error {
oasDataRaw, err := utils.ReadExternalResource(generateGatewayAPIHTTPRouteOAS)
if err != nil {
return err
}

openapiLoader := openapi3.NewLoader()
doc, err := openapiLoader.LoadFromData(oasDataRaw)
if err != nil {
return err
}

err = doc.Validate(openapiLoader.Context)
if err != nil {
return fmt.Errorf("OpenAPI validation error: %w", err)
}

ap := buildAuthPolicy(doc)

jsonData, err := json.Marshal(ap)
if err != nil {
return err
}

fmt.Fprintln(cmd.OutOrStdout(), string(jsonData))
return nil
}

func buildAuthPolicy(doc *openapi3.T) *kuadrantapiv1beta2.AuthPolicy {
routeMeta := gatewayapi.HTTPRouteObjectMetaFromOAS(doc)

ap := &kuadrantapiv1beta2.AuthPolicy{
TypeMeta: v1.TypeMeta{
APIVersion: "kuadrant.io/v1beta2",
Kind: "AuthPolicy",
},
ObjectMeta: kuadrantapi.AuthPolicyObjectMetaFromOAS(doc),
Spec: kuadrantapiv1beta2.AuthPolicySpec{
TargetRef: gatewayapiv1alpha2.PolicyTargetReference{
Group: gatewayapiv1beta1.Group("gateway.networking.k8s.io"),
Kind: gatewayapiv1beta1.Kind("HTTPRoute"),
Name: gatewayapiv1beta1.ObjectName(routeMeta.Name),
},
// Currently only authentication rules enforced
AuthScheme: kuadrantapiv1beta2.AuthSchemeSpec{
Authentication: kuadrantapi.AuthPolicyAuthenticationSchemeFromOAS(doc),
},
RouteSelectors: kuadrantapi.AuthPolicyTopRouteSelectorsFromOAS(doc),
},
}

if routeMeta.Namespace != "" {
ap.Spec.TargetRef.Namespace = &[]gatewayapiv1beta1.Namespace{
gatewayapiv1beta1.Namespace(routeMeta.Namespace),
}[0]
}

return ap
}
2 changes: 1 addition & 1 deletion cmd/generate_kuadrant_ratelimitpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/kuadrant/kuadrantctl/pkg/utils"
)

//kuadrantctl generate kuadrant httproute --oas [OAS_FILE_PATH | OAS_URL | @]
//kuadrantctl generate kuadrant ratelimitpolicy --oas [OAS_FILE_PATH | OAS_URL | @]

func generateKuadrantRateLimitPolicyCommand() *cobra.Command {
cmd := &cobra.Command{
Expand Down
251 changes: 251 additions & 0 deletions doc/generate-kuadrant-auth-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
## Generate Kuadrant AuthPolicy object from OpenAPI 3

The `kuadrantctl generate kuadrant authpolicy` command generates an [Kuadrant AuthPolicy](https://github.com/Kuadrant/kuadrant-operator/blob/v0.4.1/doc/auth.md)
from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) powered with [kuadrant extensions](openapi-kuadrant-extensions.md).

### OpenAPI specification

[OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md)

OpenAPI document resource can be provided by one of the following channels:
* Filename in the available path.
* URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address.
* Read from stdin standard input stream.

#### openIdConnect type
This initial version of the command only generates AuhPolicy when there is at least one security requirement referencing the
[Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) which type is `openIdConnect`.

### Description

The following OAS example has one protected endpoint `GET /dog` with OIDC sec scheme.

```yaml
paths:
/dog:
get:
operationId: "getDog"
security:
- securedDog: []
responses:
405:
description: "invalid input"
components:
securitySchemes:
securedDog:
type: openIdConnect
openIdConnectUrl: https://example.com/.well-known/openid-configuration
```

Running the command

```
kuadrantctl generate kuadrant authpolicy --oas ./petstore-openapi.yaml | yq -P
```

The generated authpolicy (only relevan fields shown here):

```yaml
kind: AuthPolicy
apiVersion: kuadrant.io/v1beta2
metadata:
name: petstore
namespace: petstore
creationTimestamp: null
spec:
routeSelectors:
- matches:
- path:
type: Exact
value: /api/v1/dog
method: GET
rules:
authentication:
getDog:
credentials: {}
jwt:
issuerUrl: https://example.com/.well-known/openid-configuration
routeSelectors:
- matches:
- path:
type: Exact
value: /api/v1/dog
method: GET
```

### Usage

```shell
Generate Kuadrant AuthPolicy from OpenAPI 3.0.X

Usage:
kuadrantctl generate kuadrant authpolicy [flags]

Flags:
-h, --help help for authpolicy
--oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required)

Global Flags:
-v, --verbose verbose output
```

> Under the example folder there are examples of OAS 3 that can be used to generate the resources

### User Guide

* [Optional] Setup SSO service supporting OIDC. For this example, we will be using [keycloak](https://www.keycloak.org).
* Create a new realm `petstore`
* Create a client `petstore`. In the Client Protocol field, select `openid-connect`.
* Configure client settings. Access Type to public. Direct Access Grants Enabled to ON (for this example password will be used directly to generate the token).
* Add a user to the realm
* Click the Users menu on the left side of the window. Click Add user.
* Type the username `bob`, set the Email Verified switch to ON, and click Save.
* On the Credentials tab, set the password `p`. Enter the password in both the fields, set the Temporary switch to OFF to avoid the password reset at the next login, and click `Set Password`.

Now, let's run local cluster to test the kuadrantctl new command to generate authpolicy.

* Clone the repo

```bash
git clone https://github.com/Kuadrant/kuadrantctl.git
cd kuadrantctl
```

* Setup cluster, istio and Gateway API CRDs

```bash
make local-setup
```

* Build and install CLI in `bin/kuadrantctl` path

```bash
make install
```

* Install Kuadrant service protection. The CLI can be used to install kuadrant v0.4.1

```bash
bin/kuadrantctl install
```

* Deploy petstore backend API

```bash
kubectl create namespace petstore
kubectl apply -n petstore -f examples/petstore/petstore.yaml
```

* Let's create Petstore's OpenAPI spec

<details>

```yaml
cat <<EOF >petstore-openapi.yaml
---
openapi: "3.0.3"
info:
title: "Pet Store API"
version: "1.0.0"
x-kuadrant:
route:
name: "petstore"
namespace: "petstore"
hostnames:
- example.com
parentRefs:
- name: istio-ingressgateway
namespace: istio-system
servers:
- url: https://example.io/api/v1
paths:
/cat:
x-kuadrant:
backendRefs:
- name: petstore
port: 80
namespace: petstore
get: # public (not auth)
operationId: "getCat"
responses:
405:
description: "invalid input"
/dog:
x-kuadrant:
backendRefs:
- name: petstore
port: 80
namespace: petstore
get: # secured
operationId: "getDog"
security:
- openIdConnect: []
responses:
405:
description: "invalid input"
components:
securitySchemes:
openIdConnect:
type: openIdConnect
openIdConnectUrl: https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore
EOF
```
</details>

> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain

| Operation | Applied config |
| --- | --- |
| `GET /api/v1/cat` | public (not auth) |
| `GET /api/v1/dog` | OIDC authenticatred |

* Create the HTTPRoute using the CLI
```bash
bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f -
```

* Create Kuadrant's Auth Policy
```bash
bin/kuadrantctl generate kuadrant authpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f -
```

Now, we are ready to test OpenAPI endpoints :exclamation:

- `GET /api/v1/cat` -> It's a public endpoint, hence should return 200 Ok
```bash
curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/cat"
```
- `GET /api/v1/dog` -> It's a secured endpoint, hence, without credentials, it should return 401
```bash
curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/dog"
```
```
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="getDog"
x-ext-auth-reason: credential not found
date: Tue, 28 Nov 2023 09:38:26 GMT
server: istio-envoy
content-length: 0
```
- Get authentication token. This example is using Direct Access Grants oauth2 grant type (also known as Client Credentials grant type). When configuring the Keycloak (OIDC provider) client settings, we enabled Direct Access Grants to enable this procedure. We will be authenticating as `bob` user with `p` password. We previously created `bob` user in Keycloak in the `petstore` realm.
```
export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencoded" \
-d 'grant_type=password' \
-d 'client_id=petstore' \
-d 'scope=openid' \
-d 'username=bob' \
-d 'password=p' "https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore/protocol/openid-connect/token" | jq -r '.access_token')
```
> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain

With the access token in place, let's try to get those puppies

```bash
curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/dog -i
```
should return 200 Ok

* Clean environment
```bash
make local-cleanup
```
Loading
Loading