Skip to content

Commit

Permalink
Merge pull request #46 from Kuadrant/authpolicy
Browse files Browse the repository at this point in the history
Generate Authpolicy from OpenAPI 3.0.X
  • Loading branch information
eguzki authored Nov 28, 2023
2 parents d3d31d0 + fdb047b commit 1148fc8
Show file tree
Hide file tree
Showing 9 changed files with 765 additions and 25 deletions.
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

0 comments on commit 1148fc8

Please sign in to comment.