Support talking to multiple ORY Hydra deployments (#35)

This commit is contained in:
Paul Davis 2019-11-14 01:11:13 -07:00 committed by hackerman
parent c0bc5dffa5
commit 803c935b47
17 changed files with 404 additions and 51 deletions

View File

@ -2,11 +2,11 @@ version: 2
jobs:
build:
docker:
- image: circleci/golang:1.12
- image: circleci/golang:1.13
working_directory: /go/src/github.com/ory/hydra-maester
steps:
- run:
name: Enable go1.12 modules
name: Enable go1.11 modules
command: |
echo 'export GO111MODULE=on' >> $BASH_ENV
source $BASH_ENV
@ -28,7 +28,7 @@ jobs:
- run: make
test:
docker:
- image: circleci/golang:1.12
- image: circleci/golang:1.13
environment:
- GO111MODULE=on
working_directory: /go/src/github.com/ory/hydra-maester
@ -56,8 +56,8 @@ jobs:
name: Update golang
command: |
sudo rm -rf /usr/local/go/
curl -LO https://dl.google.com/go/go1.12.7.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.12.7.linux-amd64.tar.gz
curl -LO https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.13.4.linux-amd64.tar.gz
sudo echo "export PATH=$PATH:/usr/local/go/bin" >> $HOME/.profile
go version
@ -80,7 +80,7 @@ jobs:
command: |
go get github.com/onsi/ginkgo/ginkgo
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.0-beta.2
go install sigs.k8s.io/kustomize/v3/cmd/kustomize
go install sigs.k8s.io/kustomize/kustomize/v3
- run:
name: Install Kind
@ -101,7 +101,7 @@ jobs:
release:
docker:
- image: circleci/golang:1.12
- image: circleci/golang:1.13
environment:
- GO111MODULE=on
working_directory: /go/src/github.com/ory/hydra-maester

3
.gitignore vendored
View File

@ -23,4 +23,5 @@ bin
*.swo
*~
config/default/manager_image_patch.yaml-e
config/default/manager_image_patch.yaml-e
/manager

View File

@ -49,7 +49,7 @@ generate: controller-gen
$(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/...
# Build the docker image
docker-build: test
docker-build: test manager
docker build . -t ${IMG}
@echo "updating kustomize image patch file for manager resource"
sed -i'' -e 's@image: .*@image: '"${IMG}"'@' ./config/default/manager_image_patch.yaml

View File

@ -15,9 +15,9 @@
# Hydra-maester
This project contains a Kubernetes controller that uses Custom Resources (CR) to manage Hydra Oauth2 clients. ORY Hydra Maester watches for instances of `oauth2clients.oathkeeper.ory.sh/v1alpha1` CR and creates, updates, or deletes corresponding OAuth2 clients by communicating with ORY Hydra's API.
This project contains a Kubernetes controller that uses Custom Resources (CR) to manage Hydra Oauth2 clients. ORY Hydra Maester watches for instances of `oauth2clients.hydra.ory.sh/v1alpha1` CR and creates, updates, or deletes corresponding OAuth2 clients by communicating with ORY Hydra's API.
Visit Hydra-maester's [chart documentation](https://github.com/ory/k8s/blob/master/docs/helm/hydra-maester.md) and view [sample OAuth2 client resources](config/samples) to learn more about the `oauth2clients.oathkeeper.ory.sh/v1alpha1` CR.
Visit Hydra-maester's [chart documentation](https://github.com/ory/k8s/blob/master/docs/helm/hydra-maester.md) and view [sample OAuth2 client resources](config/samples) to learn more about the `oauth2clients.hydra.ory.sh/v1alpha1` CR.
The project is based on [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder).

View File

@ -25,12 +25,46 @@ import (
type StatusCode string
const (
StatusRegistrationFailed StatusCode = "CLIENT_REGISTRATION_FAILED"
StatusCreateSecretFailed StatusCode = "SECRET_CREATION_FAILED"
StatusUpdateFailed StatusCode = "CLIENT_UPDATE_FAILED"
StatusInvalidSecret StatusCode = "INVALID_SECRET"
StatusRegistrationFailed StatusCode = "CLIENT_REGISTRATION_FAILED"
StatusCreateSecretFailed StatusCode = "SECRET_CREATION_FAILED"
StatusUpdateFailed StatusCode = "CLIENT_UPDATE_FAILED"
StatusInvalidSecret StatusCode = "INVALID_SECRET"
StatusInvalidHydraAddress StatusCode = "INVALID_HYDRA_ADDRESS"
)
// HydraAdmin defines the desired hydra admin instance to use for OAuth2Client
type HydraAdmin struct {
// +kubebuilder:validation:MaxLength=64
// +kubebuilder:validation:Pattern=(^$|^https?://.*)
//
// URL is the URL for the hydra instance on
// which to set up the client. This value will override the value
// provided to `--hydra-url`
URL string `json:"url,omitempty"`
// +kubebuilder:validation:Maximum=65535
//
// Port is the port for the hydra instance on
// which to set up the client. This value will override the value
// provided to `--hydra-port`
Port int `json:"port,omitempty"`
// +kubebuilder:validation:Pattern=(^$|^/.*)
//
// Endpoint is the endpoint for the hydra instance on which
// to set up the client. This value will override the value
// provided to `--endpoint` (defaults to `"/clients"` in the
// application)
Endpoint string `json:"endpoint,omitempty"`
// +kubebuilder:validation:Pattern=(^$|https?|off)
//
// ForwardedProto overrides the `--forwarded-proto` flag. The
// value "off" will force this to be off even if
// `--forwarded-proto` is specified
ForwardedProto string `json:"forwardedProto,omitempty"`
}
// OAuth2ClientSpec defines the desired state of OAuth2Client
type OAuth2ClientSpec struct {
// +kubebuilder:validation:MaxItems=4
@ -46,6 +80,9 @@ type OAuth2ClientSpec struct {
// use at the authorization endpoint.
ResponseTypes []ResponseType `json:"responseTypes,omitempty"`
// RedirectURIs is an array of the redirect URIs allowed for the application
RedirectURIs []RedirectURI `json:"redirectUris,omitempty"`
// +kubebuilder:validation:Pattern=([a-zA-Z0-9\.\*]+\s?)+
//
// Scope is a string containing a space-separated list of scope values (as
@ -59,6 +96,10 @@ type OAuth2ClientSpec struct {
//
// SecretName points to the K8s secret that contains this client's ID and password
SecretName string `json:"secretName"`
// HydraAdmin is the optional configuration to use for managing
// this client
HydraAdmin HydraAdmin `json:"hydraAdmin,omitempty"`
}
// +kubebuilder:validation:Enum=client_credentials;authorization_code;implicit;refresh_token
@ -69,6 +110,10 @@ type GrantType string
// ResponseType represents an OAuth 2.0 response type strings
type ResponseType string
// +kubebuilder:validation:Pattern=\w+:/?/?[^\s]+
// RedirectURI represents a redirect URI for the client
type RedirectURI string
// OAuth2ClientStatus defines the observed state of OAuth2Client
type OAuth2ClientStatus struct {
// ObservedGeneration represents the most recent generation observed by the daemon set controller.
@ -114,6 +159,7 @@ func (c *OAuth2Client) ToOAuth2ClientJSON() *hydra.OAuth2ClientJSON {
return &hydra.OAuth2ClientJSON{
GrantTypes: grantToStringSlice(c.Spec.GrantTypes),
ResponseTypes: responseToStringSlice(c.Spec.ResponseTypes),
RedirectURIs: redirectToStringSlice(c.Spec.RedirectURIs),
Scope: c.Spec.Scope,
Owner: fmt.Sprintf("%s/%s", c.Name, c.Namespace),
}
@ -134,3 +180,11 @@ func grantToStringSlice(gt []GrantType) []string {
}
return output
}
func redirectToStringSlice(ru []RedirectURI) []string {
var output = make([]string, len(ru))
for i, elem := range ru {
output[i] = string(elem)
}
return output
}

View File

@ -52,7 +52,7 @@ func TestCreateAPI(t *testing.T) {
Namespace: "default",
}
t.Run("by creating an API object if it meets CRD requirements", func(t *testing.T) {
t.Run("by creating an API object if it meets CRD requirements without optional parameters", func(t *testing.T) {
resetTestClient()
@ -71,13 +71,45 @@ func TestCreateAPI(t *testing.T) {
require.Error(t, getErr)
})
t.Run("by creating an API object if it meets CRD requirements with optional parameters", func(t *testing.T) {
resetTestClient()
created.Spec.RedirectURIs = []RedirectURI{"https://client/account", "http://localhost:8080/account"}
created.Spec.HydraAdmin = HydraAdmin{
URL: "http://localhost",
Port: 4445,
// Endpoint: "/clients",
ForwardedProto: "https",
}
createErr = k8sClient.Create(context.TODO(), created)
require.NoError(t, createErr)
fetched = &OAuth2Client{}
getErr = k8sClient.Get(context.TODO(), key, fetched)
require.NoError(t, getErr)
assert.Equal(t, created, fetched)
deleteErr = k8sClient.Delete(context.TODO(), created)
require.NoError(t, deleteErr)
getErr = k8sClient.Get(context.TODO(), key, created)
require.Error(t, getErr)
})
t.Run("by failing if the requested object doesn't meet CRD requirements", func(t *testing.T) {
for desc, modifyClient := range map[string]func(){
"invalid grant type": func() { created.Spec.GrantTypes = []GrantType{"invalid"} },
"invalid response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid"} },
"invalid scope": func() { created.Spec.Scope = "" },
"missing secret name": func() { created.Spec.SecretName = "" },
"invalid grant type": func() { created.Spec.GrantTypes = []GrantType{"invalid"} },
"invalid response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid"} },
"invalid scope": func() { created.Spec.Scope = "" },
"missing secret name": func() { created.Spec.SecretName = "" },
"invalid redirect URI": func() { created.Spec.RedirectURIs = []RedirectURI{"invalid"} },
"invalid hydra url": func() { created.Spec.HydraAdmin.URL = "invalid" },
"invalid hydra port high": func() { created.Spec.HydraAdmin.Port = 65536 },
"invalid hydra endpoint": func() { created.Spec.HydraAdmin.Endpoint = "invalid" },
"invalid hydra forwarded proto": func() { created.Spec.HydraAdmin.Endpoint = "invalid" },
} {
t.Run(fmt.Sprintf("case=%s", desc), func(t *testing.T) {

View File

@ -23,6 +23,21 @@ 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 *HydraAdmin) DeepCopyInto(out *HydraAdmin) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HydraAdmin.
func (in *HydraAdmin) DeepCopy() *HydraAdmin {
if in == nil {
return nil
}
out := new(HydraAdmin)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OAuth2Client) DeepCopyInto(out *OAuth2Client) {
*out = *in
@ -95,6 +110,12 @@ func (in *OAuth2ClientSpec) DeepCopyInto(out *OAuth2ClientSpec) {
*out = make([]ResponseType, len(*in))
copy(*out, *in)
}
if in.RedirectURIs != nil {
in, out := &in.RedirectURIs, &out.RedirectURIs
*out = make([]RedirectURI, len(*in))
copy(*out, *in)
}
out.HydraAdmin = in.HydraAdmin
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuth2ClientSpec.

View File

@ -400,6 +400,43 @@ spec:
maxItems: 4
minItems: 1
type: array
hydraAdmin:
description: HydraAdmin is the optional configuration to use for managing
this client
properties:
endpoint:
description: Endpoint is the endpoint for the hydra instance on
which to set up the client. This value will override the value
provided to `--endpoint` (defaults to `"/clients"` in the application)
pattern: (^$|^/.*)
type: string
forwardedProto:
description: ForwardedProto overrides the `--forwarded-proto` flag.
The value "off" will force this to be off even if `--forwarded-proto`
is specified
pattern: (^$|https?|off)
type: string
port:
description: Port is the port for the hydra instance on which to
set up the client. This value will override the value provided
to `--hydra-port`
maximum: 65535
type: integer
url:
description: URL is the URL for the hydra instance on which to set
up the client. This value will override the value provided to
`--hydra-url`
maxLength: 64
pattern: (^$|^https?://.*)
type: string
type: object
redirectUris:
description: RedirectURIs is an array of the redirect URIs allowed for
the application
items:
pattern: \w+:/?/?[^\s]+
type: string
type: array
responseTypes:
description: ResponseTypes is an array of the OAuth 2.0 response type
strings that the client can use at the authorization endpoint.

View File

@ -8,5 +8,5 @@ spec:
spec:
containers:
# Change the value of image field below to your controller image URL
- image: controller:latest
- image: dangersalad/hydra-maester:v0.0.5-alpha15
name: manager

View File

@ -13,5 +13,20 @@ spec:
- id_token
- code
- token
redirectUris:
- https://client/account
- http://localhost:8080
scope: "read write"
secretName: my-secret-123
# these are optional
redirectUris:
- https://client/account
- http://localhost:8080
hydraAdmin:
# if hydraAdmin is specified, all of these fields are requried,
# but they can be empty/0
url: http://hydra-admin.namespace.cluster.domain
port: 4445
endpoint: /clients
forwardedProto: https

View File

@ -8,7 +8,7 @@ data:
client_id: MDA5MDA5MDA=
client_secret: czNjUjM3cDRzc1ZWMHJEMTIzNA==
---
apiVersion: hydra.ory.sh/v1alpha1
apiVersion: hydra.ory.sh/v1alpha2
kind: OAuth2Client
metadata:
name: my-oauth2-client-2
@ -25,3 +25,14 @@ spec:
- token
scope: "read write"
secretName: my-secret-456
# these are optional
redirectUris:
- https://client/account
- http://localhost:8080
hydraAdmin:
# if hydraAdmin is specified, all of these fields are requried,
# but they can be empty/0
url: http://hydra-admin.namespace.cluster.domain
port: 4445
endpoint: /clients
forwardedProto: https

View File

@ -34,8 +34,18 @@ import (
const (
ClientIDKey = "client_id"
ClientSecretKey = "client_secret"
FinalizerName = "finalizer.ory.hydra.sh"
)
type HydraClientMakerFunc func(hydrav1alpha1.OAuth2ClientSpec) (HydraClientInterface, error)
type clientMapKey struct {
url string
port int
endpoint string
forwardedProto string
}
type HydraClientInterface interface {
GetOAuth2Client(id string) (*hydra.OAuth2ClientJSON, bool, error)
ListOAuth2Client() ([]*hydra.OAuth2ClientJSON, error)
@ -46,8 +56,10 @@ type HydraClientInterface interface {
// OAuth2ClientReconciler reconciles a OAuth2Client object
type OAuth2ClientReconciler struct {
HydraClient HydraClientInterface
Log logr.Logger
HydraClient HydraClientInterface
HydraClientMaker HydraClientMakerFunc
Log logr.Logger
otherClients map[clientMapKey]HydraClientInterface
client.Client
}
@ -62,7 +74,7 @@ func (r *OAuth2ClientReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error
var oauth2client hydrav1alpha1.OAuth2Client
if err := r.Get(ctx, req.NamespacedName, &oauth2client); err != nil {
if apierrs.IsNotFound(err) {
if registerErr := r.unregisterOAuth2Clients(ctx, req.Name, req.Namespace); registerErr != nil {
if registerErr := r.unregisterOAuth2Clients(ctx, &oauth2client); registerErr != nil {
return ctrl.Result{}, registerErr
}
return ctrl.Result{}, nil
@ -70,6 +82,38 @@ func (r *OAuth2ClientReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error
return ctrl.Result{}, err
}
// examine DeletionTimestamp to determine if object is under deletion
if oauth2client.ObjectMeta.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object. This is equivalent
// registering our finalizer.
if !containsString(oauth2client.ObjectMeta.Finalizers, FinalizerName) {
oauth2client.ObjectMeta.Finalizers = append(oauth2client.ObjectMeta.Finalizers, FinalizerName)
if err := r.Update(ctx, &oauth2client); err != nil {
return ctrl.Result{}, err
}
}
} else {
// The object is being deleted
if containsString(oauth2client.ObjectMeta.Finalizers, FinalizerName) {
// our finalizer is present, so lets handle any external dependency
if err := r.unregisterOAuth2Clients(ctx, &oauth2client); err != nil {
// if fail to delete the external dependency here, return with error
// so that it can be retried
return ctrl.Result{}, err
}
// remove our finalizer from the list and update it.
oauth2client.ObjectMeta.Finalizers = removeString(oauth2client.ObjectMeta.Finalizers, FinalizerName)
if err := r.Update(ctx, &oauth2client); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
if oauth2client.Generation != oauth2client.Status.ObservedGeneration {
var secret apiv1.Secret
@ -92,7 +136,21 @@ func (r *OAuth2ClientReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error
return ctrl.Result{}, nil
}
fetched, found, err := r.HydraClient.GetOAuth2Client(string(credentials.ID))
hydraClient, err := r.getHydraClientForClient(oauth2client)
if err != nil {
r.Log.Error(err, fmt.Sprintf(
"hydra address %s:%d%s is invalid",
oauth2client.Spec.HydraAdmin.URL,
oauth2client.Spec.HydraAdmin.Port,
oauth2client.Spec.HydraAdmin.Endpoint,
))
if updateErr := r.updateReconciliationStatusError(ctx, &oauth2client, hydrav1alpha1.StatusInvalidHydraAddress, err); updateErr != nil {
return ctrl.Result{}, updateErr
}
return ctrl.Result{}, nil
}
fetched, found, err := hydraClient.GetOAuth2Client(string(credentials.ID))
if err != nil {
return ctrl.Result{}, err
@ -128,12 +186,17 @@ func (r *OAuth2ClientReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
func (r *OAuth2ClientReconciler) registerOAuth2Client(ctx context.Context, c *hydrav1alpha1.OAuth2Client, credentials *hydra.Oauth2ClientCredentials) error {
if err := r.unregisterOAuth2Clients(ctx, c.Name, c.Namespace); err != nil {
if err := r.unregisterOAuth2Clients(ctx, c); err != nil {
return err
}
hydra, err := r.getHydraClientForClient(*c)
if err != nil {
return err
}
if credentials != nil {
if _, err := r.HydraClient.PostOAuth2Client(c.ToOAuth2ClientJSON().WithCredentials(credentials)); err != nil {
if _, err := hydra.PostOAuth2Client(c.ToOAuth2ClientJSON().WithCredentials(credentials)); err != nil {
if updateErr := r.updateReconciliationStatusError(ctx, c, hydrav1alpha1.StatusRegistrationFailed, err); updateErr != nil {
return updateErr
}
@ -141,7 +204,7 @@ func (r *OAuth2ClientReconciler) registerOAuth2Client(ctx context.Context, c *hy
return r.ensureEmptyStatusError(ctx, c)
}
created, err := r.HydraClient.PostOAuth2Client(c.ToOAuth2ClientJSON())
created, err := hydra.PostOAuth2Client(c.ToOAuth2ClientJSON())
if err != nil {
if updateErr := r.updateReconciliationStatusError(ctx, c, hydrav1alpha1.StatusRegistrationFailed, err); updateErr != nil {
return updateErr
@ -170,7 +233,12 @@ func (r *OAuth2ClientReconciler) registerOAuth2Client(ctx context.Context, c *hy
}
func (r *OAuth2ClientReconciler) updateRegisteredOAuth2Client(ctx context.Context, c *hydrav1alpha1.OAuth2Client, credentials *hydra.Oauth2ClientCredentials) error {
if _, err := r.HydraClient.PutOAuth2Client(c.ToOAuth2ClientJSON().WithCredentials(credentials)); err != nil {
hydra, err := r.getHydraClientForClient(*c)
if err != nil {
return err
}
if _, err := hydra.PutOAuth2Client(c.ToOAuth2ClientJSON().WithCredentials(credentials)); err != nil {
if updateErr := r.updateReconciliationStatusError(ctx, c, hydrav1alpha1.StatusUpdateFailed, err); updateErr != nil {
return updateErr
}
@ -178,16 +246,27 @@ func (r *OAuth2ClientReconciler) updateRegisteredOAuth2Client(ctx context.Contex
return r.ensureEmptyStatusError(ctx, c)
}
func (r *OAuth2ClientReconciler) unregisterOAuth2Clients(ctx context.Context, name, namespace string) error {
func (r *OAuth2ClientReconciler) unregisterOAuth2Clients(ctx context.Context, c *hydrav1alpha1.OAuth2Client) error {
clients, err := r.HydraClient.ListOAuth2Client()
// if a reqired field is empty, that means this is a delete after
// the finalizers have done their job, so just return
if c.Spec.Scope == "" || c.Spec.SecretName == "" {
return nil
}
hydra, err := r.getHydraClientForClient(*c)
if err != nil {
return err
}
for _, c := range clients {
if c.Owner == fmt.Sprintf("%s/%s", name, namespace) {
if err := r.HydraClient.DeleteOAuth2Client(*c.ClientID); err != nil {
clients, err := hydra.ListOAuth2Client()
if err != nil {
return err
}
for _, cJSON := range clients {
if cJSON.Owner == fmt.Sprintf("%s/%s", c.Name, c.Namespace) {
if err := hydra.DeleteOAuth2Client(*cJSON.ClientID); err != nil {
return err
}
}
@ -237,3 +316,41 @@ func parseSecret(secret apiv1.Secret) (*hydra.Oauth2ClientCredentials, error) {
Password: psw,
}, nil
}
func (r *OAuth2ClientReconciler) getHydraClientForClient(oauth2client hydrav1alpha1.OAuth2Client) (HydraClientInterface, error) {
spec := oauth2client.Spec
if spec.HydraAdmin == (hydrav1alpha1.HydraAdmin{}) {
r.Log.Info(fmt.Sprintf("using default client"))
return r.HydraClient, nil
}
key := clientMapKey{
url: spec.HydraAdmin.URL,
port: spec.HydraAdmin.Port,
endpoint: spec.HydraAdmin.Endpoint,
forwardedProto: spec.HydraAdmin.ForwardedProto,
}
if c, ok := r.otherClients[key]; ok {
return c, nil
}
return r.HydraClientMaker(spec)
}
// Helper functions to check and remove string from a slice of strings.
func containsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
func removeString(slice []string, s string) (result []string) {
for _, item := range slice {
if item == s {
continue
}
result = append(result, item)
}
return
}

View File

@ -68,6 +68,7 @@ var _ = Describe("OAuth2Client Controller", func() {
Secret: pointer.StringPtr(tstSecret),
GrantTypes: o.GrantTypes,
ResponseTypes: o.ResponseTypes,
RedirectURIs: o.RedirectURIs,
Scope: o.Scope,
Owner: o.Owner,
}
@ -211,6 +212,7 @@ var _ = Describe("OAuth2Client Controller", func() {
Secret: o.Secret,
GrantTypes: o.GrantTypes,
ResponseTypes: o.ResponseTypes,
RedirectURIs: o.RedirectURIs,
Scope: o.Scope,
Owner: o.Owner,
}
@ -366,6 +368,9 @@ func getAPIReconciler(mgr ctrl.Manager, mock controllers.HydraClientInterface) r
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("OAuth2Client"),
HydraClient: mock,
HydraClientMaker: func(hydrav1alpha1.OAuth2ClientSpec) (controllers.HydraClientInterface, error) {
return mock, nil
},
}
}
@ -380,6 +385,13 @@ func testInstance(name, secretName string) *hydrav1alpha1.OAuth2Client {
GrantTypes: []hydrav1alpha1.GrantType{"client_credentials"},
ResponseTypes: []hydrav1alpha1.ResponseType{"token"},
Scope: "a b c",
RedirectURIs: []hydrav1alpha1.RedirectURI{"https://example.com"},
SecretName: secretName,
HydraAdmin: hydrav1alpha1.HydraAdmin{
URL: "http://hydra-admin",
Port: 4445,
Endpoint: "/client",
ForwardedProto: "https",
},
}}
}

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/ory/hydra-maester
go 1.12
go 1.13
require (
github.com/go-logr/logr v0.1.0

View File

@ -11,8 +11,9 @@ import (
)
type Client struct {
HydraURL url.URL
HTTPClient *http.Client
HydraURL url.URL
HTTPClient *http.Client
ForwardedProto string
}
func (c *Client) GetOAuth2Client(id string) (*OAuth2ClientJSON, bool, error) {
@ -148,6 +149,10 @@ func (c *Client) newRequest(method, relativePath string, body interface{}) (*htt
return nil, err
}
if c.ForwardedProto != "" {
req.Header.Add("X-Forwarded-Proto", c.ForwardedProto)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}

View File

@ -7,6 +7,7 @@ type OAuth2ClientJSON struct {
ClientID *string `json:"client_id,omitempty"`
Secret *string `json:"client_secret,omitempty"`
GrantTypes []string `json:"grant_types"`
RedirectURIs []string `json:"redirect_uris,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope"`
Owner string `json:"owner"`

73
main.go
View File

@ -47,16 +47,17 @@ func init() {
}
func main() {
var metricsAddr string
var hydraURL string
var hydraPort int
var endpoint string
var enableLeaderElection bool
var (
metricsAddr, hydraURL, endpoint, forwardedProto string
hydraPort int
enableLeaderElection bool
)
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&hydraURL, "hydra-url", "", "The address of ORY Hydra")
flag.IntVar(&hydraPort, "hydra-port", 4445, "Port ORY Hydra is listening on")
flag.StringVar(&endpoint, "endpoint", "/clients", "ORY Hydra's client endpoint")
flag.StringVar(&forwardedProto, "forwarded-proto", "", "If set, this adds the value as the X-Forwarded-Proto header in requests to the ORY Hydra admin server")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
flag.Parse()
@ -78,19 +79,27 @@ func main() {
os.Exit(1)
}
u, err := url.Parse(fmt.Sprintf("%s:%d", hydraURL, hydraPort))
defaultSpec := hydrav1alpha1.OAuth2ClientSpec{
HydraAdmin: hydrav1alpha1.HydraAdmin{
URL: hydraURL,
Port: hydraPort,
Endpoint: endpoint,
ForwardedProto: forwardedProto,
},
}
hydraClientMaker := getHydraClientMaker(defaultSpec)
hydraClient, err := hydraClientMaker(defaultSpec)
if err != nil {
setupLog.Error(err, "unable to parse ORY Hydra's URL", "controller", "OAuth2Client")
setupLog.Error(err, "making default hydra client", "controller", "OAuth2Client")
os.Exit(1)
}
err = (&controllers.OAuth2ClientReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("OAuth2Client"),
HydraClient: &hydra.Client{
HydraURL: *u.ResolveReference(&url.URL{Path: endpoint}),
HTTPClient: &http.Client{},
},
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("OAuth2Client"),
HydraClient: hydraClient,
HydraClientMaker: hydraClientMaker,
}).SetupWithManager(mgr)
if err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OAuth2Client")
@ -104,3 +113,41 @@ func main() {
os.Exit(1)
}
}
func getHydraClientMaker(defaultSpec hydrav1alpha1.OAuth2ClientSpec) controllers.HydraClientMakerFunc {
return controllers.HydraClientMakerFunc(func(spec hydrav1alpha1.OAuth2ClientSpec) (controllers.HydraClientInterface, error) {
if spec.HydraAdmin.URL == "" {
spec.HydraAdmin.URL = defaultSpec.HydraAdmin.URL
}
if spec.HydraAdmin.Port == 0 {
spec.HydraAdmin.Port = defaultSpec.HydraAdmin.Port
}
if spec.HydraAdmin.Endpoint == "" {
spec.HydraAdmin.Endpoint = defaultSpec.HydraAdmin.Endpoint
}
if spec.HydraAdmin.ForwardedProto == "" {
spec.HydraAdmin.ForwardedProto = defaultSpec.HydraAdmin.ForwardedProto
}
address := fmt.Sprintf("%s:%d", spec.HydraAdmin.URL, spec.HydraAdmin.Port)
u, err := url.Parse(address)
if err != nil {
return nil, fmt.Errorf("unable to parse ORY Hydra's URL: %w", err)
}
client := &hydra.Client{
HydraURL: *u.ResolveReference(&url.URL{Path: spec.HydraAdmin.Endpoint}),
HTTPClient: &http.Client{},
}
if spec.HydraAdmin.ForwardedProto != "" && spec.HydraAdmin.ForwardedProto != "off" {
client.ForwardedProto = spec.HydraAdmin.ForwardedProto
}
return client, nil
})
}