diff --git a/apis/capabilities/v1beta1/application_types.go b/apis/capabilities/v1beta1/application_types.go index 00c3f681f..2d4ee2685 100644 --- a/apis/capabilities/v1beta1/application_types.go +++ b/apis/capabilities/v1beta1/application_types.go @@ -59,6 +59,11 @@ type ApplicationSpec struct { // Suspend application if true suspends application, if false resumes application. //+optional Suspend bool `json:"suspend,omitempty"` + + // AuthSecretRef reference to the API credentials secret. This secret is + // used only once when creating a new application + //+optional + AuthSecretRef *corev1.LocalObjectReference `json:"authSecretRef"` } // ApplicationStatus defines the observed state of Application diff --git a/apis/capabilities/v1beta1/zz_generated.deepcopy.go b/apis/capabilities/v1beta1/zz_generated.deepcopy.go index 94f343eb7..c90073ec2 100644 --- a/apis/capabilities/v1beta1/zz_generated.deepcopy.go +++ b/apis/capabilities/v1beta1/zz_generated.deepcopy.go @@ -520,6 +520,11 @@ func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { *out = new(v1.LocalObjectReference) **out = **in } + if in.AuthSecretRef != nil { + in, out := &in.AuthSecretRef, &out.AuthSecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSpec. diff --git a/bundle/manifests/capabilities.3scale.net_applications.yaml b/bundle/manifests/capabilities.3scale.net_applications.yaml index 59d57a273..64f34dab4 100644 --- a/bundle/manifests/capabilities.3scale.net_applications.yaml +++ b/bundle/manifests/capabilities.3scale.net_applications.yaml @@ -55,6 +55,19 @@ spec: applicationPlanName: description: ApplicationPlanName name of application plan that the application will use type: string + authSecretRef: + description: |- + AuthSecretRef reference to the API credentials secret. This secret is + used only once when creating a new application + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic description: description: Description human-readable text of the application type: string diff --git a/config/crd/bases/capabilities.3scale.net_applications.yaml b/config/crd/bases/capabilities.3scale.net_applications.yaml index 2f604e9b9..0f32fd44c 100644 --- a/config/crd/bases/capabilities.3scale.net_applications.yaml +++ b/config/crd/bases/capabilities.3scale.net_applications.yaml @@ -58,6 +58,19 @@ spec: description: ApplicationPlanName name of application plan that the application will use type: string + authSecretRef: + description: |- + AuthSecretRef reference to the API credentials secret. This secret is + used only once when creating a new application + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + type: object + x-kubernetes-map-type: atomic description: description: Description human-readable text of the application type: string diff --git a/controllers/capabilities/application_controller.go b/controllers/capabilities/application_controller.go index 4e663999d..e183fb2b9 100644 --- a/controllers/capabilities/application_controller.go +++ b/controllers/capabilities/application_controller.go @@ -243,7 +243,29 @@ func (r *ApplicationReconciler) applicationReconciler(applicationResource *capab return nil, err } - reconciler := NewApplicationReconciler(r.BaseReconciler, applicationResource, *accountResource.Status.ID, *productResource.Status.ID, threescaleAPIClient) + var authParams map[string]string + if applicationResource.Spec.AuthSecretRef != nil { + authSecretObj, err := helper.GetSecret(applicationResource.Spec.AuthSecretRef.Name, applicationResource.Namespace, r.Client()) + if err != nil { + return nil, err + } + + authMode, err := extractApplicationCredentialType(productResource) + if err != nil { + return nil, err + } + + if err := validateApplicationCrendentialSecret(authSecretObj, authMode); err != nil { + return nil, err + } + + authParams, err = handleCredentials(authSecretObj, authMode) + if err != nil { + return nil, err + } + } + + reconciler := NewApplicationReconciler(r.BaseReconciler, applicationResource, authParams, *accountResource.Status.ID, *productResource.Status.ID, threescaleAPIClient) return reconciler.Reconcile() } diff --git a/controllers/capabilities/application_credentials.go b/controllers/capabilities/application_credentials.go new file mode 100644 index 000000000..cd6f735bb --- /dev/null +++ b/controllers/capabilities/application_credentials.go @@ -0,0 +1,115 @@ +package controllers + +import ( + "fmt" + + capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ThreescaleCredentialTypeUserKey = "1" + ThreescaleCredentialTypeAppID = "2" + ThreescaleCredentialTypeOIDC = "oidc" +) + +const ( + ThreescaleCredentialParamUserKey = "user_key" + ThreescaleCredentialParamAppID = "application_id" + ThreescaleCredentialParamAppKey = "application_key" +) + +const ( + CredentialSecretKeyNameUserKey = "UserKey" + CredentialSecretKeyNameAppID = "ApplicationID" + CredentialSecretKeyNameAppKey = "ApplicationKey" + CredentialSecretKeyNameClientID = "ClientID" + CredentialSecretKeyNameClientSecret = "ClientSecret" +) + +// extractApplicationCredentialType returns the credential type from the product +// setting +func extractApplicationCredentialType(productResource *capabilitiesv1beta1.Product) (string, error) { + credType := productResource.Spec.AuthenticationMode() + if credType == nil { + return "", fmt.Errorf("unable to identify authentication mode from Product CR") + } + return *credType, nil +} + +func validateApplicationCrendentialSecret(s *corev1.Secret, authMode string) error { + nn := client.ObjectKeyFromObject(s) + + switch authMode { + case ThreescaleCredentialTypeUserKey: + if err := validateSecretForAuthModeUserKey(s); err != nil { + return err + } + case ThreescaleCredentialTypeAppID: + if err := validateSecretForAuthModeAppIDAppKey(s); err != nil { + return err + } + case ThreescaleCredentialTypeOIDC: + if err := validateSecretForAuthModeOIDC(s); err != nil { + return err + } + default: + return fmt.Errorf("secret %s used, but has unsupported type %s", nn, authMode) + } + return nil +} + +func validateSecretForAuthModeUserKey(s *corev1.Secret) error { + if _, ok := s.Data[CredentialSecretKeyNameUserKey]; !ok { + return fmt.Errorf("secret %s used as user-key authentication mode, but lacks %s key", + client.ObjectKeyFromObject(s), CredentialSecretKeyNameUserKey, + ) + } + return nil +} + +func validateSecretForAuthModeAppIDAppKey(s *corev1.Secret) error { + if _, ok := s.Data[CredentialSecretKeyNameAppID]; !ok { + return fmt.Errorf("secret %s used as app-id/app-key authentication mode, but lacks %s key", + client.ObjectKeyFromObject(s), CredentialSecretKeyNameAppID, + ) + } + if _, ok := s.Data[CredentialSecretKeyNameAppKey]; !ok { + return fmt.Errorf("secret %s used as app-id/app-key authentication mode, but lacks %s key", + client.ObjectKeyFromObject(s), CredentialSecretKeyNameAppKey, + ) + } + return nil +} + +func validateSecretForAuthModeOIDC(s *corev1.Secret) error { + if _, ok := s.Data[CredentialSecretKeyNameClientID]; !ok { + return fmt.Errorf("secret %s used as oidc authentication mode, but lacks %s key", + client.ObjectKeyFromObject(s), CredentialSecretKeyNameClientID, + ) + } + if _, ok := s.Data[CredentialSecretKeyNameClientSecret]; !ok { + return fmt.Errorf("secret %s used as oidc authentication mode, but lacks %s key", + client.ObjectKeyFromObject(s), CredentialSecretKeyNameClientSecret, + ) + } + return nil +} + +func handleCredentials(creds *corev1.Secret, authType string) (map[string]string, error) { + authParams := make(map[string]string) + switch authType { + case ThreescaleCredentialTypeUserKey: + authParams[ThreescaleCredentialParamUserKey] = string(creds.Data[CredentialSecretKeyNameUserKey]) + case ThreescaleCredentialTypeAppID: + authParams[ThreescaleCredentialParamAppID] = string(creds.Data[CredentialSecretKeyNameAppID]) + authParams[ThreescaleCredentialParamAppKey] = string(creds.Data[CredentialSecretKeyNameAppKey]) + case ThreescaleCredentialTypeOIDC: + authParams[ThreescaleCredentialParamAppID] = string(creds.Data[CredentialSecretKeyNameClientID]) + authParams[ThreescaleCredentialParamAppKey] = string(creds.Data[CredentialSecretKeyNameClientSecret]) + default: + return nil, fmt.Errorf("unknown authentication mode") + } + return authParams, nil +} diff --git a/controllers/capabilities/application_threescale_reconciler.go b/controllers/capabilities/application_threescale_reconciler.go index 80d5805b5..f4c22882a 100644 --- a/controllers/capabilities/application_threescale_reconciler.go +++ b/controllers/capabilities/application_threescale_reconciler.go @@ -17,16 +17,18 @@ type ApplicationThreescaleReconciler struct { *reconcilers.BaseReconciler applicationResource *capabilitiesv1beta1.Application applicationEntity *controllerhelper.ApplicationEntity + authParams map[string]string accountID int64 productID int64 threescaleAPIClient *threescaleapi.ThreeScaleClient logger logr.Logger } -func NewApplicationReconciler(b *reconcilers.BaseReconciler, applicationResource *capabilitiesv1beta1.Application, accountID int64, productID int64, threescaleAPIClient *threescaleapi.ThreeScaleClient) *ApplicationThreescaleReconciler { +func NewApplicationReconciler(b *reconcilers.BaseReconciler, applicationResource *capabilitiesv1beta1.Application, authParams map[string]string, accountID int64, productID int64, threescaleAPIClient *threescaleapi.ThreeScaleClient) *ApplicationThreescaleReconciler { return &ApplicationThreescaleReconciler{ BaseReconciler: b, applicationResource: applicationResource, + authParams: authParams, accountID: accountID, productID: productID, threescaleAPIClient: threescaleAPIClient, @@ -113,6 +115,12 @@ func (t *ApplicationThreescaleReconciler) syncApplication(_ any) error { "name": t.applicationResource.Spec.Name, "description": t.applicationResource.Spec.Description, } + + if t.authParams != nil { + for key, value := range t.authParams { + params.AddParam(key, value) + } + } // Application doesn't exist yet - create it a, err := t.threescaleAPIClient.CreateApplication(t.accountID, plan.Element.ID, t.applicationResource.Spec.Name, params) if err != nil { @@ -133,6 +141,12 @@ func (t *ApplicationThreescaleReconciler) syncApplication(_ any) error { "description": t.applicationResource.Spec.Description, } + if t.authParams != nil { + for key, value := range t.authParams { + params.AddParam(key, value) + } + } + // Application doesn't exist yet - create it a, err := t.threescaleAPIClient.CreateApplication(t.accountID, plan.Element.ID, t.applicationResource.Spec.Name, params) if err != nil { diff --git a/doc/application-reference.md b/doc/application-reference.md index 208e3e74a..8b5dcec8b 100644 --- a/doc/application-reference.md +++ b/doc/application-reference.md @@ -28,6 +28,7 @@ Created by [github-markdown-toc](https://github.com/ekalinin/github-markdown-toc | ProductCR | `productCR` | object | name of product CR via [v1.LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#localobjectreference-v1-core) | Yes | | ApplicationPlanName | `applicationPlanName` | string | name of application plan that the application will use | Yes | | Suspend | `suspend` | bool | suspend application if true suspends application, if false resumes application | No | +| AuthSecretRef | `authSecretRef` | object | [Auth secret reference](#Auth-secret-reference) | No | @@ -36,6 +37,56 @@ Created by [github-markdown-toc](https://github.com/ekalinin/github-markdown-toc Application CR relies on the provider account reference for the [developer account](./developeruser-reference.md#provider-account-reference) and the [product](./product-reference.md#provider-account-reference) being the same. If not you will see an error in the status. +#### Auth secret reference + +Auth secret reference by a [v1.LocalObjectReference](https://v1-15.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#localobjectreference-v1-core) type object. + +| **Field** | **Description** | **Required** | +| ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------------ | +| *UserKey* | UserKey field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes | +| *ApplicationID* | ApplicationID field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes | +| *ApplicationKey* | ApplicationKey field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes | +| *ClientID* | ClientID field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes | +| *ClientSecret* | ClientSecret field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes | + + +NOTE: ApplicationCR relies on ProductCR authentication mode to determine which fields to use from the secret + +For example: +* With UserKey authentication mode +``` +apiVersion: v1 +kind: Secret +metadata: + name: authsecret +type: Opaque +stringData: + UserKey: "testApplicationUserKey" +``` + +* With AppID/AppKey authentication mode +``` +apiVersion: v1 +kind: Secret +metadata: + name: authsecret +type: Opaque +stringData: + ApplicationID: "testApplicationID" + ApplicationKey: "testApplicationKey" +``` + +* With OIDC authentication mode +``` +apiVersion: v1 +kind: Secret +metadata: + name: authsecret +type: Opaque +stringData: + ClientID: "testApplicationClientID" + ClientSecret: "testApplicationClientSecret" +``` ### ApplicationStatus diff --git a/doc/operator-application-capabilities.md b/doc/operator-application-capabilities.md index 02e785649..c7345b614 100644 --- a/doc/operator-application-capabilities.md +++ b/doc/operator-application-capabilities.md @@ -1672,6 +1672,11 @@ spec: systemName: hits backend: backend1 name: product1 + deployment: + apicastHosted: + authentication: + appKeyAppID + appID: token backendUsages: backend1: path: / @@ -1734,6 +1739,41 @@ status: providerAccountHost: 'https://3scale-admin.example.com' state: suspended ``` + +By default, 3scale automatically generates a random and immutable ID for each application upon creation. + +If you require an application to have a predefined ID, you must supply an authentication secret. + +``` +apiVersion: v1 +kind: Secret +metadata: + name: authsecret +type: Opaque +stringData: + ApplicationID: "testApplicationID" + ApplicationKey: "testApplicationKey" +``` + +Reference the secret with `authSecretRef` + +```yaml +apiVersion: capabilities.3scale.net/v1beta1 +kind: Application +metadata: + name: example +spec: + accountCR: + name: developeraccount01 + applicationPlanName: plan02 + productCR: + name: product1-cr + name: testApp12 + description: further testing12 + suspend: true + authSecretRef: authsecret +``` + [Application CRD reference](application-reference.md) for more info about fields. ### Application Custom Resource Status Fields