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 kuadrant authconfig #37

Merged
merged 8 commits into from
Jan 31, 2022
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ go install github.com/kuadrant/kuadrantctl@latest
* [Apply Kuadrant API objects](doc/api-apply.md)
* [Generate Istio virtualservice objects](doc/generate-istio-virtualservice.md)
* [Generate Istio authenticationpolicy objects](doc/generate-istio-authorizationpolicy.md)
* [Generate kuadrat authconfig objects](doc/generate-kuadrant-authconfig.md)

## Contributing
The [Development guide](doc/development.md) describes how to build the kuadrantctl CLI and how to test your changes before submitting a patch or opening a PR.
Expand Down
1 change: 1 addition & 0 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func generateCommand() *cobra.Command {
}

cmd.AddCommand(generateIstioCommand())
cmd.AddCommand(generateKuadrantCommand())

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

import (
"github.com/spf13/cobra"
)

func generateKuadrantCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "kuadrant",
Short: "Generate Kuadrant resources",
Long: "Generate Kuadrant resources",
}

cmd.AddCommand(generateKuadrantAuthconfigCommand())

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

import (
"encoding/json"
"fmt"

"github.com/getkin/kin-openapi/openapi3"
authorinov1beta1 "github.com/kuadrant/authorino/api/v1beta1"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

authorinoutils "github.com/kuadrant/kuadrantctl/pkg/authorino"
"github.com/kuadrant/kuadrantctl/pkg/utils"
)

var (
generateKuadrantAuthConfigOAS string
generateKuadrantAuthConfigPublicHost string
)

func generateKuadrantAuthconfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "authconfig",
Short: "Generate kuadrant authconfig from OpenAPI 3.x",
Long: "Generate kuadrant authconfig from OpenAPI 3.x",
RunE: func(cmd *cobra.Command, args []string) error {
return runGenerateKuadrantAuthconfigCommand(cmd, args)
},
}

// OpenAPI ref
cmd.Flags().StringVar(&generateKuadrantAuthConfigOAS, "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)
}

// public host
cmd.Flags().StringVar(&generateKuadrantAuthConfigPublicHost, "public-host", "", "The address used by a client when attempting to connect to a service (required)")
err = cmd.MarkFlagRequired("public-host")
if err != nil {
panic(err)
}

return cmd
}

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

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

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

authConfig, err := generateKuadrantAuthConfig(cmd, doc)
if err != nil {
return err
}

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

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

func generateKuadrantAuthConfig(cmd *cobra.Command, doc *openapi3.T) (*authorinov1beta1.AuthConfig, error) {
objectName, err := utils.K8sNameFromOpenAPITitle(doc)
if err != nil {
return nil, err
}

identityList, err := authorinoutils.AuthConfigIdentitiesFromOpenAPI(doc)
if err != nil {
return nil, err
}

metadataList, err := generateKuadrantAuthConfigMetadata(doc)
if err != nil {
return nil, err
}

authorizationList, err := generateKuadrantAuthConfigAuthorization(doc)
if err != nil {
return nil, err
}

responseList, err := generateKuadrantAuthConfigResponse(doc)
if err != nil {
return nil, err
}

authConfig := &authorinov1beta1.AuthConfig{
TypeMeta: metav1.TypeMeta{
Kind: "AuthConfig",
APIVersion: "authorino.kuadrant.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: objectName,
},
Spec: authorinov1beta1.AuthConfigSpec{
Hosts: []string{generateKuadrantAuthConfigPublicHost},
Identity: identityList,
Metadata: metadataList,
Authorization: authorizationList,
Response: responseList,
Patterns: nil,
Conditions: nil,
},
}
return authConfig, nil
}

func generateKuadrantAuthConfigMetadata(doc *openapi3.T) ([]*authorinov1beta1.Metadata, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the empty methods for wondering what the metadata concept is?

Copy link
Collaborator Author

@eguzki eguzki Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

placeholders if we ever need to generate them. We can remove them and assign the value nil in the struct

return nil, nil
}

func generateKuadrantAuthConfigAuthorization(doc *openapi3.T) ([]*authorinov1beta1.Authorization, error) {
return nil, nil
}

func generateKuadrantAuthConfigResponse(doc *openapi3.T) ([]*authorinov1beta1.Response, error) {
return nil, nil
}
29 changes: 29 additions & 0 deletions doc/generate-kuadrant-authconfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Generate Kuadrant AuthConfig objects

The `kuadrantctl generate kuadrant authconfig` command generates an [Authorino AuthConfig](https://github.com/Kuadrant/authorino/blob/v0.7.0/docs/architecture.md#the-authorino-authconfig-custom-resource-definition-crd)
from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) and kubernetes service information.

### OpenAPI specification

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.

### Usage :

```shell
$ kuadrantctl generate kuadrant authconfig -h
Generate kuadrant authconfig from OpenAPI 3.x

Usage:
kuadrantctl generate kuadrant authconfig [flags]

Flags:
-h, --help help for authconfig
--oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR - (required)
--public-host string The address used by a client when attempting to connect to a service (required)

Global Flags:
-v, --verbose verbose output
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.16
require (
github.com/getkin/kin-openapi v0.76.0
github.com/google/uuid v1.3.0
github.com/kuadrant/authorino v0.7.0
github.com/kuadrant/authorino-operator v0.1.0
github.com/kuadrant/kuadrant-controller v0.2.1
github.com/kuadrant/limitador-operator v0.2.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kuadrant/authorino v0.7.0 h1:YgeFzteyzlfFZqWZN3b7cXbyOZZ2vgHP2UHp7yktrvg=
github.com/kuadrant/authorino v0.7.0/go.mod h1:+ddl2McmSC8vKufmR/iA4ILOwkTCe5dbX+uzWSZ4w1Q=
github.com/kuadrant/authorino-operator v0.1.0 h1:2MluwjhdtQl/z3C5BkM7BMvQr8WRzlUrDRe96DYdq6w=
github.com/kuadrant/authorino-operator v0.1.0/go.mod h1:enEBTG0San0QUHhqZ08OXiVDkURkRE2UBEQelcNTiGI=
Expand Down
134 changes: 134 additions & 0 deletions pkg/authorino/auth_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package authorino

import (
"fmt"
"strings"

"github.com/getkin/kin-openapi/openapi3"
authorinov1beta1 "github.com/kuadrant/authorino/api/v1beta1"

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

func AuthConfigIdentitiesFromOpenAPI(oasDoc *openapi3.T) ([]*authorinov1beta1.Identity, error) {
identities := []*authorinov1beta1.Identity{}

workloadName, err := utils.K8sNameFromOpenAPITitle(oasDoc)
if err != nil {
return nil, err
}

for path, pathItem := range oasDoc.Paths {
for opVerb, operation := range pathItem.Operations() {
secReqsP := utils.OpenAPIOperationSecRequirements(oasDoc, operation)

if secReqsP == nil {
continue
}

for _, secReq := range *secReqsP {
// Authorino AuthConfig currently only supports one identity method for each identity evaluator.
// It does not support, for instance, auth based on two api keys or api key AND oidc.
// Thus, some OpenAPI 3.X security requirements are not supported:
//
// Not Supported:
// security:
// - petstore_api_key: []
// toystore_api_key: []
// toystore_oidc: []
//
// Supported:
// security:
// - petstore_api_key: []
// - toystore_api_key: []
// - toystore_oidc: []
//

// scopes not being used now

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you see a use case for scopes in the future? Are you thinking it might make up part of authorization? Or would we leave that entirely up to the user to implement?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually part of the spec, so the command should generate required config to validate the scopes. The authorino implementation would be in the authorization phase validating that the user doing the request has granted those scopes.

I think we can leave this for another PR. Definitely is a missing feature from the command

for secSchemeName := range secReq {

secSchemeI, err := oasDoc.Components.SecuritySchemes.JSONLookup(secSchemeName)
if err != nil {
return nil, err
}

secScheme := secSchemeI.(*openapi3.SecurityScheme) // panic if assertion fails

identity, err := AuthConfigIdentityFromSecurityRequirement(
operation.OperationID, // TODO(eastizle): OperationID can be null, fallback to some custom name
path, opVerb, workloadName, secScheme)
if err != nil {
return nil, err
}

identities = append(identities, identity)
// currently only support for one schema per requirement
break
}
}

}
}
return identities, nil
}

func AuthConfigConditionsFromOperation(opPath, opVerb string) []authorinov1beta1.JSONPattern {
return []authorinov1beta1.JSONPattern{
{
JSONPatternExpression: authorinov1beta1.JSONPatternExpression{
Selector: `context.request.http.path.@extract:{"sep":"/"}`,
Operator: "eq",
Value: opPath,
},
},
{
JSONPatternExpression: authorinov1beta1.JSONPatternExpression{
Selector: "context.request.http.method.@case:lower",
Operator: "eq",
Value: strings.ToLower(opVerb),
},
},
}
}

func AuthConfigIdentityFromSecurityRequirement(name, opPath, opVerb, workloadName string, secScheme *openapi3.SecurityScheme) (*authorinov1beta1.Identity, error) {
if secScheme == nil {
return nil, fmt.Errorf("sec scheme nil for operation path:%s method:%s", opPath, opVerb)
}

identity := &authorinov1beta1.Identity{
Name: name,
Conditions: AuthConfigConditionsFromOperation(opPath, opVerb),
}

switch secScheme.Type {
case "apiKey":
AuthConfigIdentityFromApiKeyScheme(identity, secScheme, workloadName)
case "openIdConnect":

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have missed it on the call earlier. Are these the only two schemas in open API?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there are more:
For OpenAPI 3.0.X it is "apiKey", "http", , "oauth2", "openIdConnect".
OpenAPI 3.1 added "mutualTLS"

For the first implementation, apiKey and oidc. Others can be implemented either as authorizationpolicies or with authorino (authorino does not implement, yet, mtls)

Copy link
Contributor

@guicassolato guicassolato Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think oauth2 could be supported combining Envoy's OAuth2 filter and either Authorino's OIDC or OAuth2 Token Introspection.

http can be modelled on top of Authorino API key authn. See this user guide for ref.

And, yes, mutualTLS soon to be supported in Authorino, but not yet.

AuthConfigIdentityFromOIDCScheme(identity, secScheme)
default:
return nil, fmt.Errorf("sec scheme type %s not supported for path:%s method:%s", secScheme.Type, opPath, opVerb)
}

return identity, nil
}

func AuthConfigIdentityFromApiKeyScheme(identity *authorinov1beta1.Identity, secScheme *openapi3.SecurityScheme, workloadName string) {
// Fixed label selector for now
apikey := authorinov1beta1.Identity_APIKey{
LabelSelectors: map[string]string{
"authorino.kuadrant.io/managed-by": "authorino",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can users be more restrictive here?

The way it is right now, virtually any API key secret stored in a cluster can be used to authenticate to any API protected with Authorino deployed by Kuadrant in this cluster.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add some more to be more restrictive

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a new label app = {title of the openapi satinized}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, the CLI can have some default values.. as a user I would expect to override them if I needed.

For another PR (good exercise to somebody wanting to learn) add CLI optional parameters to override default labels.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a question for Authorino. but should it not be limited to looking in the same namespace as the authconfig? Seems odd I would protect my API with secret in a different namespace?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That depends on the deployment mode of authorino. As kuadrant deployment, it will be cluster scoped controller, so secrets on any namespace are valid as long as they have the required labels.

Copy link

@maleck13 maleck13 Jan 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd to me though, that I can create an authconfig in namespace A with a secret label selector "app":"myapp" and someone working in a different namespace can create a secret in their namespace with the same selector and then get access to my API? They don't need to know the API Key or contents of the secret they just need to be able to guess the label selector. This is not related to the PR though so lets discuss separately

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"app": workloadName,
},
}

identity.Credentials.In = authorinov1beta1.Credentials_In(secScheme.In)
identity.Credentials.KeySelector = secScheme.Name
identity.APIKey = &apikey
}

func AuthConfigIdentityFromOIDCScheme(identity *authorinov1beta1.Identity, secScheme *openapi3.SecurityScheme) {
identity.Oidc = &authorinov1beta1.Identity_OidcConfig{
Endpoint: secScheme.OpenIdConnectUrl,
}
}