diff --git a/.circleci/config.yml b/.circleci/config.yml index 5295ee0..6b7dde4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.gitignore b/.gitignore index 18298d8..862e9a7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ bin *.swo *~ -config/default/manager_image_patch.yaml-e \ No newline at end of file +config/default/manager_image_patch.yaml-e +/manager diff --git a/Makefile b/Makefile index be6661a..59a8110 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 7664f84..43b49a6 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/api/v1alpha1/oauth2client_types.go b/api/v1alpha1/oauth2client_types.go index bf81d90..94d9309 100644 --- a/api/v1alpha1/oauth2client_types.go +++ b/api/v1alpha1/oauth2client_types.go @@ -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 +} diff --git a/api/v1alpha1/oauth2client_types_test.go b/api/v1alpha1/oauth2client_types_test.go index 61d9982..fa61e04 100644 --- a/api/v1alpha1/oauth2client_types_test.go +++ b/api/v1alpha1/oauth2client_types_test.go @@ -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) { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d2b2e34..9e8f1c7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -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. diff --git a/config/crd/bases/hydra.ory.sh_oauth2clients.yaml b/config/crd/bases/hydra.ory.sh_oauth2clients.yaml index 249bfc2..5aead69 100644 --- a/config/crd/bases/hydra.ory.sh_oauth2clients.yaml +++ b/config/crd/bases/hydra.ory.sh_oauth2clients.yaml @@ -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. diff --git a/config/default/manager_image_patch.yaml b/config/default/manager_image_patch.yaml index b84e6c1..143ed41 100644 --- a/config/default/manager_image_patch.yaml +++ b/config/default/manager_image_patch.yaml @@ -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 diff --git a/config/samples/hydra_v1alpha1_oauth2client.yaml b/config/samples/hydra_v1alpha1_oauth2client.yaml index f77bee0..ee8013d 100644 --- a/config/samples/hydra_v1alpha1_oauth2client.yaml +++ b/config/samples/hydra_v1alpha1_oauth2client.yaml @@ -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 + diff --git a/config/samples/hydra_v1alpha1_oauth2client_user_credentials.yaml b/config/samples/hydra_v1alpha1_oauth2client_user_credentials.yaml index 1394351..885adda 100644 --- a/config/samples/hydra_v1alpha1_oauth2client_user_credentials.yaml +++ b/config/samples/hydra_v1alpha1_oauth2client_user_credentials.yaml @@ -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 diff --git a/controllers/oauth2client_controller.go b/controllers/oauth2client_controller.go index e47ae6d..5c506e5 100644 --- a/controllers/oauth2client_controller.go +++ b/controllers/oauth2client_controller.go @@ -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 +} diff --git a/controllers/oauth2client_controller_integration_test.go b/controllers/oauth2client_controller_integration_test.go index 1ca2e7a..407f79a 100644 --- a/controllers/oauth2client_controller_integration_test.go +++ b/controllers/oauth2client_controller_integration_test.go @@ -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", + }, }} } diff --git a/go.mod b/go.mod index f9de68b..e765adc 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/hydra/client.go b/hydra/client.go index 2f2e401..5659768 100644 --- a/hydra/client.go +++ b/hydra/client.go @@ -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") } diff --git a/hydra/types.go b/hydra/types.go index 7a14801..1da6c42 100644 --- a/hydra/types.go +++ b/hydra/types.go @@ -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"` diff --git a/main.go b/main.go index 1c76d8b..3391755 100644 --- a/main.go +++ b/main.go @@ -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 + }) + +}