-
Notifications
You must be signed in to change notification settings - Fork 14
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
Changes from all commits
c6346b5
bb3b7f3
b35b52e
752effa
3a49ab3
886460e
d8f5f27
7d6edc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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) { | ||
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 | ||
} |
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 | ||
``` |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, there are more: For the first implementation, apiKey and oidc. Others can be implemented either as authorizationpolicies or with authorino (authorino does not implement, yet, mtls) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
And, yes, |
||
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add some more to be more restrictive There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a new label There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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