diff --git a/README.md b/README.md index b2c56cfe..1a594447 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ To deploy the controller, edit the value of the `--hydra-url` argument in the | ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------- | | **hydra-url** | yes | ORY Hydra's service address | - | ` ory-hydra-admin.ory.svc.cluster.local` | | **hydra-port** | no | ORY Hydra's service port | `4445` | `4445` | +| **endpoint** | no | ORY Hydra's client endpoint | `/clients` | `clients` | | **tls-trust-store** | no | TLS cert path for hydra client | `""` | `/etc/ssl/certs/ca-certificates.crt` | | **insecure-skip-verify** | no | Skip http client insecure verification | `false` | `true` or `false` | | **namespace** | no | Namespace in which the controller should operate. Setting this will make the controller ignore other namespaces. | `""` | `"my-namespace"` | diff --git a/api/v1alpha1/oauth2client_types.go b/api/v1alpha1/oauth2client_types.go index 57345964..235b8564 100644 --- a/api/v1alpha1/oauth2client_types.go +++ b/api/v1alpha1/oauth2client_types.go @@ -49,6 +49,20 @@ type HydraAdmin struct { // value "off" will force this to be off even if // `--forwarded-proto` is specified ForwardedProto string `json:"forwardedProto,omitempty"` + + // ApiKeySecretRef is an object to define the secret which contains + // Ory Network API Key + ApiKeySecretRef ApiKeySecretRef `json:"apiKeySecretRef,omitempty"` +} + +// ApiKeySecretRef contains Secret details for the API Key +type ApiKeySecretRef struct { + // Name of the secret containing the API Key + Name string `json:"name,omitempty"` + // Key of the secret for the API key + Key string `json:"key,omitempty"` + // Namespace of the secret if different from hydra-maester controller + Namespace string `json:"namespace,omitempty"` } // OAuth2ClientSpec defines the desired state of OAuth2Client diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 18f8abf7..062e2fb4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,9 +26,25 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApiKeySecretRef) DeepCopyInto(out *ApiKeySecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiKeySecretRef. +func (in *ApiKeySecretRef) DeepCopy() *ApiKeySecretRef { + if in == nil { + return nil + } + out := new(ApiKeySecretRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HydraAdmin) DeepCopyInto(out *HydraAdmin) { *out = *in + out.ApiKeySecretRef = in.ApiKeySecretRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HydraAdmin. diff --git a/config/crd/bases/hydra.ory.sh_oauth2clients.yaml b/config/crd/bases/hydra.ory.sh_oauth2clients.yaml index e896e947..d5e5bf84 100644 --- a/config/crd/bases/hydra.ory.sh_oauth2clients.yaml +++ b/config/crd/bases/hydra.ory.sh_oauth2clients.yaml @@ -83,6 +83,23 @@ spec: HydraAdmin is the optional configuration to use for managing this client properties: + apiKeySecretRef: + description: + ApiKeySecretRef is an object to define the secret which + contains Ory Network API Key + properties: + key: + description: Key of the secret for the API key + type: string + name: + description: Name of the secret containing the API Key + type: string + namespace: + description: + Namespace of the secret if different from + hydra-maester controller + type: string + type: object endpoint: description: Endpoint is the endpoint for the hydra instance on which diff --git a/config/samples/hydra_v1alpha1_oauth2client_ory_network.yaml b/config/samples/hydra_v1alpha1_oauth2client_ory_network.yaml new file mode 100644 index 00000000..0d511df4 --- /dev/null +++ b/config/samples/hydra_v1alpha1_oauth2client_ory_network.yaml @@ -0,0 +1,39 @@ +apiVersion: hydra.ory.sh/v1alpha1 +kind: OAuth2Client +metadata: + name: my-oauth2-client + namespace: default +spec: + grantTypes: + - client_credentials + - implicit + - authorization_code + - refresh_token + responseTypes: + - id_token + - code + - token + - code token + - code id_token + - id_token token + - code id_token token + scope: "read write" + secretName: my-secret-123 + # these are optional + redirectUris: + - https://client/account + - http://localhost:8080 + postLogoutRedirectUris: + - https://client/logout + audience: + - audience-a + - audience-b + hydraAdmin: + # if hydraAdmin is specified, all of these fields are requried, + # but they can be empty/0 + url: https://foobar.projects.oryapis.com + endpoint: /admin/clients + port: 443 + apiKeySecretRef: + name: ory_network_key + tokenEndpointAuthMethod: client_secret_basic diff --git a/controllers/oauth2client_controller.go b/controllers/oauth2client_controller.go index 36968fae..7efec978 100644 --- a/controllers/oauth2client_controller.go +++ b/controllers/oauth2client_controller.go @@ -30,10 +30,16 @@ const ( ) type clientKey struct { - url string - port int - endpoint string - forwardedProto string + url string + port int + endpoint string + forwardedProto string + apiKeySecretRef ApiKeySecretRef +} + +type ApiKeySecretRef struct { + Name string + Namespace string } // OAuth2ClientFactory is a function that creates oauth2 client. @@ -413,6 +419,22 @@ func (r *OAuth2ClientReconciler) getHydraClientForClient( endpoint: spec.HydraAdmin.Endpoint, forwardedProto: spec.HydraAdmin.ForwardedProto, } + + secretName := determineApiSecretName(&spec.HydraAdmin) + secretNamespace := determineApiSecretNamespace(&oauth2client) + + if secretName != "" && secretNamespace != "" { + key.apiKeySecretRef = ApiKeySecretRef{ + Name: secretName, + Namespace: secretNamespace, + } + spec.HydraAdmin.ApiKeySecretRef.Name = secretName + spec.HydraAdmin.ApiKeySecretRef.Namespace = secretNamespace + if spec.HydraAdmin.ApiKeySecretRef.Key == "" { + spec.HydraAdmin.ApiKeySecretRef.Key = "hydra_api_key" + } + } + r.mu.Lock() defer r.mu.Unlock() if c, ok := r.oauth2Clients[key]; ok { @@ -457,3 +479,17 @@ func removeString(slice []string, s string) (result []string) { } return } + +func determineApiSecretName(spec *hydrav1alpha1.HydraAdmin) string { + if spec.ApiKeySecretRef.Name != "" { + return spec.ApiKeySecretRef.Name + } + return "" +} + +func determineApiSecretNamespace(spec *hydrav1alpha1.OAuth2Client) string { + if spec.Spec.HydraAdmin.ApiKeySecretRef.Namespace != "" { + return spec.Spec.HydraAdmin.ApiKeySecretRef.Namespace + } + return spec.ObjectMeta.Namespace +} diff --git a/hydra/client.go b/hydra/client.go index 1a48aa4b..2db45377 100644 --- a/hydra/client.go +++ b/hydra/client.go @@ -5,15 +5,22 @@ package hydra import ( "bytes" + "context" "encoding/json" "fmt" "io" "net/http" "net/url" + "os" "path" hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1" "github.com/ory/hydra-maester/helpers" + apiv1 "k8s.io/api/core/v1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) type Client interface { @@ -28,6 +35,7 @@ type InternalClient struct { HydraURL url.URL HTTPClient *http.Client ForwardedProto string + ApiKey string } // New returns a new hydra InternalClient instance. @@ -52,6 +60,18 @@ func New(spec hydrav1alpha1.OAuth2ClientSpec, tlsTrustStore string, insecureSkip client.ForwardedProto = spec.HydraAdmin.ForwardedProto } + apiKey, err := fetchApiKey(spec.HydraAdmin.ApiKeySecretRef) + if err != nil { + return nil, err + } + + getEnv := os.Getenv("HYDRA_API_KEY") + if getEnv != "" { + client.ApiKey = getEnv + } else if apiKey != "" { + client.ApiKey = apiKey + } + return client, nil } @@ -186,6 +206,10 @@ func (c *InternalClient) newRequest(method, relativePath string, body interface{ req.Header.Add("X-Forwarded-Proto", c.ForwardedProto) } + if c.ApiKey != "" { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.ApiKey)) + } + if body != nil { req.Header.Set("Content-Type", "application/json") } @@ -207,3 +231,39 @@ func (c *InternalClient) do(req *http.Request, v interface{}) (*http.Response, e } return resp, err } + +func fetchApiKey(spec hydrav1alpha1.ApiKeySecretRef) (string, error) { + + secretName := spec.Name + secretNamespace := spec.Namespace + + if secretName == "" || secretNamespace == "" { + return "", nil + } + + cfg := ctrl.GetConfigOrDie() + kubeClient, err := client.New(cfg, client.Options{}) + if err != nil { + ctrl.Log.Error(err, "unable to create client to fetch secret") + return "", err + } + + var secret apiv1.Secret + err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: secretNamespace}, &secret) + if err != nil { + if apierrs.IsNotFound(err) { + ctrl.Log.Error(err, fmt.Sprintf("secret %s/%s does not exist", secretName, secretNamespace)) + return "", fmt.Errorf("secret %s/%s does not exist", secretNamespace, secretName) + } + ctrl.Log.Error(err, fmt.Sprintf("error fetching secret %s/%s", secretName, secretNamespace)) + return "", fmt.Errorf("error fetching secret %s/%s: %s", secretNamespace, secretName, err) + } + + secretValue, ok := secret.Data[spec.Key] + if !ok { + ctrl.Log.Error(err, fmt.Sprintf("key %s doesn't exist within secret %s/%s", secretName, secretNamespace, spec.Key)) + return "", fmt.Errorf("key %s doesn't exist within secret %s/%s", secretName, secretNamespace, spec.Key) + } + + return string(secretValue), nil +}