diff --git a/AUTH.md b/AUTH.md new file mode 100644 index 00000000..4ee91592 --- /dev/null +++ b/AUTH.md @@ -0,0 +1,646 @@ +# Authentication Configuration + +This document describes the authentication architecture and configuration variables for the Tackle/Konveyor operator. + +## Overview + +Starting with this release, the Hub acts as the primary OIDC (OpenID Connect) provider. The UI authenticates against Hub's built-in OIDC endpoints at `/oidc`. Hub supports three authentication modes: + +1. **Pure Hub OIDC** - Users managed directly in Hub (default) +2. **Federated OIDC** - Delegate to external identity providers (Keycloak, RHSSO, RHBK) +3. **LDAP/Active Directory** - Direct authentication against LDAP servers + +### Architecture + +**Pure Hub OIDC (default for new installations):** +``` +Browser → UI Route (/oidc proxy) → Hub OIDC → Hub API +``` + +**Hub OIDC with Federated Authentication:** +``` +Browser → UI Route (/oidc proxy) → Hub OIDC → [Federated IDP] → Hub API + ↓ + IdentityProvider CR +``` + +**Hub OIDC with LDAP Authentication:** +``` +Browser → UI Route (/oidc proxy) → Hub OIDC → [LDAP Server] → Hub API + ↓ + LdapProvider CR +``` + +### Key Components + +- **Hub OIDC Provider**: Built-in OAuth 2.0 / OIDC server in Hub + - Endpoints: `/oidc/authorize`, `/oidc/token`, `/oidc/.well-known/openid-configuration` + - Enabled when `feature_auth_required: true` + - Issuer URL: `https:///oidc` (external route, proxied to Hub service) + +- **UI Route/Ingress Proxy**: Existing UI route includes `/oidc` path that proxies to Hub service `/oidc` + - No separate Hub route needed + - Browser accesses: `https:///oidc` + - Proxies to: `http://:8080/oidc` + +- **IdpClient CR**: Defines OIDC client applications that can authenticate with Hub + - CRD: `tackle.konveyor.io/v1alpha1/IdpClient` + - Automatically created for web-ui, kantra, and kai-ide + - Can be extended for custom client applications + +- **IdentityProvider CR**: Configures Hub to federate authentication to external identity providers + - CRD: `tackle.konveyor.io/v1alpha1/IdentityProvider` + - Hub reads these CRs from its namespace at startup/runtime + - Used for Keycloak, RHSSO, RHBK, or any OIDC-compatible provider + +- **LdapProvider CR**: Configures LDAP/Active Directory authentication + - CRD: `tackle.konveyor.io/v1alpha1/LdapProvider` + - Provides direct LDAP authentication and authorization + - Supports role mappings from LDAP groups to application roles + +--- + +## Environment Variables + +### UI Container Environment Variables + +| Variable | Description | Example Value | Set By | +|----------|-------------|---------------|--------| +| `AUTH_REQUIRED` | Enable/disable authentication | `"true"` or `"false"` | `feature_auth_required` | +| `OIDC_ISSUER` | Hub's OIDC issuer URL (external route) | `"https://tackle.apps.example.com/oidc"` | Runtime (from UI Route/Ingress hostname) | +| `OIDC_CLIENT_ID` | UI's OIDC client identifier | `"web-ui"` | `ui_oidc_client_id` | + +**Notes:** +- `OIDC_ISSUER` uses the **external** route URL (same URL the browser uses) +- This ensures strict issuer matching - JWT tokens issued by Hub contain the same issuer URL that UI validates against +- Both browser and UI backend use the same public route URL + +### Hub Container Environment Variables + +| Variable | Description | Example Value | Set By | +|----------|-------------|---------------|--------| +| `AUTH_REQUIRED` | Enable/disable authentication | `"true"` or `"false"` | `feature_auth_required` | +| `OIDC_ISSUER` | Hub's own OIDC issuer URL (external route) | `"https://tackle.apps.example.com/oidc"` | Runtime (from UI Route/Ingress hostname) | +| `APIKEY_SECRET` | Secret key for signing API keys and JWT tokens | `""` | Generated once, stored in Hub secret | + +**Notes:** +- `OIDC_ISSUER` must match the UI's `OIDC_ISSUER` exactly for proper OIDC compliance +- `APIKEY_SECRET` is generated once and persisted in the Hub secret (same pattern as `ADDON_TOKEN`) + +--- + +## Tackle CR Variables + +### Authentication Control + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `feature_auth_required` | boolean | `false` (konveyor)
`true` (mta) | Enable/disable authentication globally | + +### UI OIDC Configuration + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `ui_oidc_client_id` | string | `"web-ui"` | OIDC client identifier for the UI | + +### Federated Identity Provider Configuration + +Configure these variables to enable federated authentication to an external identity provider: + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `keycloak_sso_url` | string | `""` | External Keycloak server URL (e.g., `"https://keycloak.example.com"`) | +| `rhbk_url` | string | `""` | External RHBK server URL (e.g., `"https://rhbk.example.com"`) | +| `keycloak_sso_realm` | string | `"{{ app_name }}"` | Keycloak realm name | +| `keycloak_sso_client_id` | string | `"{{ app_name }}-ui"` | Client ID in the federated identity provider | + +**Notes:** +- Set **either** `keycloak_sso_url` **or** `rhbk_url` to enable federated authentication +- If both are empty, pure Hub OIDC is used (no federation) +- The operator automatically detects existing operator-deployed Keycloak instances and constructs service URLs +- These variables are deprecated for deployment purposes but retained for federation configuration + +### Runtime Variables (Set by Operator) + +These variables are set at runtime by the operator and should not be configured in the Tackle CR: + +| Variable | Description | +|----------|-------------| +| `hub_oidc_issuer` | Set from UI Route/Ingress hostname + `/oidc` | +| `federated_idp_issuer` | Constructed from detection or explicit config | +| `federated_idp_client_id` | Defaults to `keycloak_sso_client_id` | + +--- + +## Configuration Scenarios + +### Scenario 1: Pure Hub OIDC (No External Identity Provider) + +**Default for fresh installations.** + +```yaml +apiVersion: tackle.konveyor.io/v1alpha2 +kind: Tackle +metadata: + name: tackle + namespace: konveyor-tackle +spec: + feature_auth_required: true +``` + +**Result:** +- Hub OIDC provider enabled +- No IdentityProvider CR created +- Users authenticate directly against Hub +- User accounts managed in Hub + +### Scenario 2: Federated Authentication to External Keycloak + +**For users who manage their own Keycloak instance.** + +```yaml +apiVersion: tackle.konveyor.io/v1alpha2 +kind: Tackle +metadata: + name: tackle + namespace: konveyor-tackle +spec: + feature_auth_required: true + keycloak_sso_url: "https://keycloak.example.com" + keycloak_sso_realm: "tackle" + keycloak_sso_client_id: "tackle-ui" +``` + +**Result:** +- Hub OIDC provider enabled +- IdentityProvider CR created pointing to external Keycloak +- Hub federates authentication to Keycloak +- Users authenticate via Hub → Keycloak +- User accounts managed in Keycloak + +**Requirements:** +1. Ensure the `tackle-ui` client exists in your Keycloak realm +2. Add redirect URI to the client: `https:///oidc/callback` +3. Client should use PKCE flow (public client, no secret required) + +### Scenario 3: Existing Operator-Deployed Keycloak (Upgrade Path) + +**For existing installations where the operator previously deployed Keycloak.** + +The operator automatically detects existing operator-deployed Keycloak instances by checking for: +- Standalone Keycloak service (labels: `app.kubernetes.io/part-of={app_name}` AND `app.kubernetes.io/component=sso`) +- RHSSO Keycloak CR (label: `app={app_name}-rhsso`, MTA profile only) +- RHBK Keycloak CR (name: `{app_name}-keycloak`, MTA profile only) + +**No Tackle CR changes needed.** + +**Result:** +- Hub OIDC provider enabled +- IdentityProvider CR created automatically pointing to existing Keycloak service +- Hub federates authentication to existing Keycloak +- Existing users continue to work +- Keycloak instance is no longer managed by the operator (remains in place) + +**Post-Upgrade Steps:** +1. Ensure the existing `tackle-ui` (or `{app_name}-ui`) client in Keycloak has the redirect URI: `https:///oidc/callback` + +### Scenario 4: LDAP/Active Directory Authentication + +**For organizations using corporate LDAP or Active Directory.** + +```yaml +apiVersion: tackle.konveyor.io/v1alpha2 +kind: Tackle +metadata: + name: tackle + namespace: konveyor-tackle +spec: + feature_auth_required: true +``` + +Then create an LdapProvider CR (see "LdapProvider Custom Resource" section for full examples). + +**Result:** +- Hub OIDC provider enabled +- Users authenticate with LDAP credentials +- Groups are synced from LDAP and mapped to application roles +- No external Keycloak needed + +### Scenario 5: Authentication Disabled + +**For development or air-gapped environments.** + +```yaml +apiVersion: tackle.konveyor.io/v1alpha2 +kind: Tackle +metadata: + name: tackle + namespace: konveyor-tackle +spec: + feature_auth_required: false +``` + +**Result:** +- No authentication required +- All API requests allowed +- Not recommended for production + +--- + +## IdentityProvider Custom Resource + +The `IdentityProvider` CR configures Hub to federate authentication to an external OIDC-compatible identity provider. + +### Example + +```yaml +apiVersion: tackle.konveyor.io/v1alpha1 +kind: IdentityProvider +metadata: + name: tackle-federated-idp + namespace: konveyor-tackle +spec: + name: federated-idp + issuer: "https://keycloak.example.com/realms/tackle" + clientId: "tackle-ui" + redirectURI: "https://tackle.apps.example.com/oidc/callback" + scopes: + - openid + - profile + - email + tls: + insecure: true +``` + +### Fields + +- `name`: Identifier for this provider (used in Hub logs) +- `issuer`: OIDC issuer URL of the external identity provider +- `clientId`: Client ID in the external identity provider +- `redirectURI`: Callback URL where the external IDP redirects after authentication (Hub OIDC callback endpoint) +- `scopes`: OIDC scopes to request from the external IDP + +**Notes:** +- Hub reads all `IdentityProvider` CRs in its namespace +- Multiple providers can be configured (Hub will use the first one found) +- Client secret is **not required** - Hub uses PKCE flow with the existing public client +- The operator creates this CR automatically based on federated IDP configuration +- `tls.insecure: true` allows Hub to connect to identity providers using self-signed certificates + +--- + +## IdpClient Custom Resource + +The `IdpClient` CR defines OIDC client applications that can authenticate with Hub's OIDC provider. The operator automatically creates client configurations for built-in applications. + +### Pre-configured Clients + +The operator automatically creates these clients when `feature_auth_required: true`: + +1. **web-ui** (ID: 1) + - Application type: web + - Grants: JWT bearer, authorization code, refresh token + - Used by the web UI + +2. **kantra** (ID: 2) + - Application type: native + - Grants: device code, authorization code, refresh token + - Used by the kantra CLI tool + +3. **kai-ide** (ID: 3) + - Application type: native + - Grants: JWT bearer, authorization code, refresh token + - Redirect URIs: `vscode://konveyor.konveyor-core/auth`, `http://127.0.0.1/callback` + - Used by IDE extensions + +### Example: Custom Client + +```yaml +apiVersion: tackle.konveyor.io/v1alpha1 +kind: IdpClient +metadata: + name: my-custom-app + namespace: konveyor-tackle +spec: + id: 100 # Must be >= 1000 for custom clients (< 1000 reserved for seeded clients) + clientId: "my-app" + applicationType: native + grants: + - authorization_code + - refresh_token + redirectURIs: + - "http://localhost:8080/callback" + scopes: + - openid + - profile + - email + clientSecret: # Optional - for confidential clients only + name: my-app-secret + namespace: konveyor-tackle +``` + +### Fields + +- `id` (integer, required): Database ID for the client. IDs 1-999 are reserved for operator-seeded clients. Custom clients must use IDs >= 1000. +- `clientId` (string): OAuth client identifier (e.g., "my-app") +- `applicationType` (string): OAuth application type - "web" or "native" +- `grants` ([]string): OAuth grant types supported by this client + - Common values: `authorization_code`, `refresh_token`, `urn:ietf:params:oauth:grant-type:jwt-bearer`, `urn:ietf:params:oauth:grant-type:device_code` +- `redirectURIs` ([]string): Valid redirect URIs for OAuth flows +- `scopes` ([]string): OAuth scopes requested by this client (e.g., `openid`, `profile`, `email`, `offline_access`) +- `clientSecret` (object, optional): Reference to a Kubernetes Secret containing the client secret + - Only needed for confidential clients (typically server-side web applications) + - Public clients (native apps, SPAs) should use PKCE instead + +**Notes:** +- The operator creates IdpClient CRs automatically for built-in applications +- Custom clients can be created by users for additional integrations +- Public clients (native apps) don't require a client secret when using PKCE + +--- + +## LdapProvider Custom Resource + +The `LdapProvider` CR configures LDAP or Active Directory authentication and authorization. When configured, Hub authenticates users against the LDAP server and maps LDAP groups to application roles. + +### Example: Standard LDAP + +```yaml +apiVersion: tackle.konveyor.io/v1alpha1 +kind: LdapProvider +metadata: + name: tackle-ldap + namespace: konveyor-tackle +spec: + name: corporate-ldap + url: "ldap://ldap.example.com:389" + baseDN: "dc=example,dc=com" + bindDN: "cn=service-account,dc=example,dc=com" + password: + name: ldap-bind-password + namespace: konveyor-tackle + userFilter: "(uid=%s)" + groupFilter: "(memberUid=%s)" + roleMappings: + - any: + - "cn=architects,ou=groups,dc=example,dc=com" + roles: + - architect + - any: + - "cn=admins,ou=groups,dc=example,dc=com" + roles: + - admin + tls: + insecure: false + ca: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- +``` + +### Example: Active Directory + +```yaml +apiVersion: tackle.konveyor.io/v1alpha1 +kind: LdapProvider +metadata: + name: tackle-ad + namespace: konveyor-tackle +spec: + name: corporate-ad + kind: ACTIVEDIRECTORY # or "AD" + url: "ldaps://ad.example.com:636" + baseDN: "dc=corp,dc=example,dc=com" + bindDN: "CN=Service Account,OU=ServiceAccounts,DC=corp,DC=example,DC=com" + password: + name: ad-bind-password + namespace: konveyor-tackle + hasMemberOf: true # Use memberOf attribute for faster group lookups + roleMappings: + - any: + - "CN=MTA-Architects,OU=Groups,DC=corp,DC=example,DC=com" + roles: + - architect + - and: + - "CN=MTA-Users,OU=Groups,DC=corp,DC=example,DC=com" + - "CN=Engineering,OU=Groups,DC=corp,DC=example,DC=com" + roles: + - migrator + tls: + insecure: true # For self-signed certificates (development only) +``` + +### Fields + +- `name` (string): Provider identifier (used in Hub logs) +- `url` (string): LDAP server URL (e.g., `ldap://host:389` or `ldaps://host:636`) +- `baseDN` (string): Base DN for LDAP searches (e.g., `dc=example,dc=com`) +- `bindDN` (string): Service account bind DN for LDAP authentication +- `password` (object): Reference to a Kubernetes Secret containing the bind password + - Secret must have a key with the password value +- `kind` (string, optional): LDAP kind - `ACTIVEDIRECTORY`, `AD`, or blank for standard LDAP + - Affects default filters and search behavior +- `userFilter` (string, optional): Custom user search filter + - Default for LDAP: `(uid=%s)` + - Default for AD: `(sAMAccountName=%s)` + - `%s` is replaced with the username +- `groupFilter` (string, optional): Custom group search filter + - Default for LDAP: `(memberUid=%s)` + - Default for AD: `(member=%s)` + - `%s` is replaced with the user DN +- `hasMemberOf` (boolean, optional): Use `memberOf` attribute for group membership + - Faster if available (common in Active Directory) + - Falls back to group filter if false or not available +- `roleMappings` ([]object): Map LDAP groups to application roles + - `any` ([]string): Match if user is in ANY of these groups (OR condition) + - `and` ([]string): Match if user is in ALL of these groups (AND condition) + - `roles` ([]string): Roles to assign when matched +- `tls` (object): TLS connection settings + - `insecure` (boolean): Skip certificate verification (development only) + - `ca` (string): PEM-encoded CA certificate for custom CAs + +**Notes:** +- LDAP provider works alongside Hub's OIDC provider +- Users authenticate with their LDAP credentials +- Group memberships are synced and mapped to application roles +- Multiple role mappings can be configured with different group patterns +- Active Directory users should set `kind: ACTIVEDIRECTORY` and `hasMemberOf: true` for best performance + +--- + +## Migration from Previous Versions + +### For Existing Deployments with Operator-Deployed Keycloak + +**Automatic upgrade - no action required.** + +The operator will: +1. Detect your existing Keycloak deployment +2. Stop managing/updating the Keycloak deployment (leaves it in place) +3. Create an IdentityProvider CR pointing to your existing Keycloak service +4. Configure Hub as OIDC provider with federation to your Keycloak +5. Update UI to use Hub OIDC + +**Post-upgrade:** +1. Verify the redirect URI in your Keycloak client includes: `https:///oidc/callback` +2. Test authentication with an existing user +3. (Optional) If you want to remove Keycloak: + - Migrate users: export/import, configure LdapProvider CR (see "LdapProvider Custom Resource" section), or recreate in Hub + - Delete the IdentityProvider CR + - Delete the Keycloak deployment manually + - System transitions to pure Hub OIDC or LDAP authentication + +### For Existing Deployments with External Keycloak + +If you were previously using an external Keycloak (not deployed by the operator), update your Tackle CR to set the federated IDP variables: + +```yaml +spec: + feature_auth_required: true + keycloak_sso_url: "https://your-keycloak.example.com" + keycloak_sso_realm: "your-realm" + keycloak_sso_client_id: "your-client-id" +``` + +Then ensure your Keycloak client has the redirect URI: `https:///oidc/callback` + +--- + +## Troubleshooting + +### Authentication fails with "Invalid issuer" + +**Cause:** The issuer in JWT tokens doesn't match the OIDC issuer URL. + +**Solution:** Verify both Hub and UI have the same `OIDC_ISSUER` environment variable value, and it matches the external route URL. + +```bash +kubectl get route tackle -n konveyor-tackle -o jsonpath='{.spec.host}' +# Should match: https:///oidc + +kubectl get deployment tackle-hub -n konveyor-tackle -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="OIDC_ISSUER")].value}' +kubectl get deployment tackle-ui -n konveyor-tackle -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="OIDC_ISSUER")].value}' +``` + +### Federated authentication not working + +**Check IdentityProvider CR exists:** +```bash +kubectl get identityprovider -n konveyor-tackle +``` + +**Check Hub logs for federation errors:** +```bash +kubectl logs deployment/tackle-hub -n konveyor-tackle | grep -i oidc +``` + +**Verify redirect URI in external identity provider:** +- The client must have redirect URI: `https:///oidc/callback` +- The client should be configured as a public client (PKCE flow) + +### OIDC discovery endpoint not found + +**Test the OIDC discovery endpoint:** +```bash +curl https:///oidc/.well-known/openid-configuration +``` + +If this fails, check: +1. UI Route/Ingress exists and is accessible +2. Route includes path `/oidc` proxied to Hub service +3. Hub deployment is running and has `AUTH_REQUIRED=true` + +--- + +## Security Considerations + +1. **OIDC Issuer URL**: Must use HTTPS in production (external route URL) +2. **APIKEY_SECRET**: Generated once and stored securely in Kubernetes secret +3. **Client Secret**: Not required - Hub uses PKCE flow for federated authentication +4. **Token Validation**: Strict issuer matching ensures tokens from other sources are rejected +5. **External IDP**: Ensure your external identity provider (Keycloak/RHSSO/RHBK) is properly secured + +--- + +## API Reference + +### Hub OIDC Endpoints + +All endpoints are relative to the UI route (proxied to Hub service): + +| Endpoint | Description | +|----------|-------------| +| `GET /oidc/.well-known/openid-configuration` | OIDC discovery document | +| `GET /oidc/authorize` | OAuth 2.0 authorization endpoint | +| `POST /oidc/token` | OAuth 2.0 token endpoint | +| `GET /oidc/callback` | Callback endpoint for federated authentication | + +### Custom Resource Definitions + +#### IdentityProvider CRD + +**API Group:** `tackle.konveyor.io/v1alpha1` + +**Kind:** `IdentityProvider` + +**Short Name:** `idp` + +**Scope:** Namespaced + +**Spec Fields:** +- `name` (string): Provider identifier +- `issuer` (string): OIDC issuer URL of the external identity provider +- `clientId` (string): Client ID in the external IDP +- `clientSecret` (object, optional): Reference to Kubernetes Secret containing client secret +- `redirectURI` (string): Callback URL where external IDP redirects after authentication +- `scopes` ([]string): OIDC scopes to request from the external IDP +- `tls` (object): TLS connection settings + - `insecure` (boolean): Skip certificate verification (for self-signed certs) + - `ca` (string): PEM-encoded CA certificate for custom CAs + +#### IdpClient CRD + +**API Group:** `tackle.konveyor.io/v1alpha1` + +**Kind:** `IdpClient` + +**Short Name:** `client` + +**Scope:** Namespaced + +**Spec Fields:** +- `id` (integer, required): Database ID (1-999 reserved, >= 1000 for custom clients) +- `clientId` (string): OAuth client identifier +- `applicationType` (string): OAuth application type ("web" or "native") +- `grants` ([]string): OAuth grant types supported by this client +- `redirectURIs` ([]string): Valid redirect URIs for OAuth flows +- `scopes` ([]string): OAuth scopes requested by this client +- `clientSecret` (object, optional): Reference to Kubernetes Secret containing client secret + +#### LdapProvider CRD + +**API Group:** `tackle.konveyor.io/v1alpha1` + +**Kind:** `LdapProvider` + +**Short Name:** `ldap` + +**Scope:** Namespaced + +**Spec Fields:** +- `name` (string): Provider identifier +- `url` (string): LDAP server URL +- `baseDN` (string): Base DN for LDAP searches +- `bindDN` (string): Service account bind DN +- `password` (object): Reference to Kubernetes Secret containing bind password +- `kind` (string, optional): LDAP kind ("ACTIVEDIRECTORY", "AD", or blank) +- `userFilter` (string, optional): Custom user search filter +- `groupFilter` (string, optional): Custom group search filter +- `hasMemberOf` (boolean, optional): Use memberOf attribute for group membership +- `roleMappings` ([]object): Map LDAP groups to application roles + - `any` ([]string): Match if user is in ANY of these groups + - `and` ([]string): Match if user is in ALL of these groups + - `roles` ([]string): Roles to assign when matched +- `tls` (object): TLS connection settings + - `insecure` (boolean): Skip certificate verification + - `ca` (string): PEM-encoded CA certificate for custom CAs diff --git a/bundle/manifests/konveyor-operator.clusterserviceversion.yaml b/bundle/manifests/konveyor-operator.clusterserviceversion.yaml index 40352422..c093c526 100644 --- a/bundle/manifests/konveyor-operator.clusterserviceversion.yaml +++ b/bundle/manifests/konveyor-operator.clusterserviceversion.yaml @@ -148,7 +148,7 @@ metadata: categories: Modernization & Migration certified: "false" containerImage: quay.io/konveyor/tackle2-operator:latest - createdAt: "2026-04-29T10:07:56Z" + createdAt: "2026-05-26T19:45:10Z" description: Konveyor is an open-source application modernization platform that helps organizations safely and predictably modernize applications to Kubernetes at scale. @@ -201,6 +201,15 @@ spec: kind: Extension name: extensions.tackle.konveyor.io version: v1alpha1 + - kind: IdentityProvider + name: identityproviders.tackle.konveyor.io + version: v1alpha1 + - kind: IdpClient + name: idpclients.tackle.konveyor.io + version: v1alpha1 + - kind: LdapProvider + name: ldapproviders.tackle.konveyor.io + version: v1alpha1 - kind: Schema name: schemas.tackle.konveyor.io version: v1alpha1 @@ -331,8 +340,6 @@ spec: value: konveyor - name: VERSION value: 99.0.0 - - name: RELATED_IMAGE_OAUTH_PROXY - value: quay.io/openshift/origin-oauth-proxy:latest - name: RELATED_IMAGE_TACKLE_HUB value: quay.io/konveyor/tackle2-hub:latest - name: RELATED_IMAGE_TACKLE_POSTGRES @@ -498,6 +505,9 @@ spec: - tackles/finalizers - addons - extensions + - identityproviders + - idpclients + - ldapproviders - tasks - schemas verbs: @@ -538,8 +548,6 @@ spec: name: Konveyor url: https://www.konveyor.io relatedImages: - - image: quay.io/openshift/origin-oauth-proxy:latest - name: oauth-proxy - image: quay.io/konveyor/tackle2-hub:latest name: tackle-hub - image: quay.io/sclorg/postgresql-15-c9s:latest diff --git a/bundle/manifests/tackle.konveyor.io_identityproviders.yaml b/bundle/manifests/tackle.konveyor.io_identityproviders.yaml new file mode 100644 index 00000000..2a222f32 --- /dev/null +++ b/bundle/manifests/tackle.konveyor.io_identityproviders.yaml @@ -0,0 +1,202 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.0 + creationTimestamp: null + name: identityproviders.tackle.konveyor.io +spec: + group: tackle.konveyor.io + names: + kind: IdentityProvider + listKind: IdentityProviderList + plural: identityproviders + shortNames: + - idp + singular: identityprovider + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IdentityProvider defines external IDP federation settings. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the resource. + properties: + clientId: + description: Client ID. + type: string + clientSecret: + description: Client secret reference (optional for public clients). + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + issuer: + description: Issuer URL. + type: string + name: + description: Provider name. + type: string + redirectURI: + description: Redirect URI. + type: string + scopes: + description: OAuth scopes (optional, provider injects defaults if + empty). + items: + type: string + type: array + tls: + description: TLS connection settings. + properties: + ca: + description: |- + CA is a PEM-encoded CA certificate for validating the server certificate. + Use when the server uses a certificate signed by an internal/private CA. + type: string + insecure: + description: |- + Insecure skips server certificate verification. + Use only for development/testing with self-signed certificates. + type: boolean + type: object + required: + - clientId + - issuer + - name + - redirectURI + type: object + status: + description: Status defines the observed state of the resource. + properties: + conditions: + description: Resource conditions. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: The most recent generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/tackle.konveyor.io_idpclients.yaml b/bundle/manifests/tackle.konveyor.io_idpclients.yaml new file mode 100644 index 00000000..5b7148b2 --- /dev/null +++ b/bundle/manifests/tackle.konveyor.io_idpclients.yaml @@ -0,0 +1,205 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.0 + creationTimestamp: null + name: idpclients.tackle.konveyor.io +spec: + group: tackle.konveyor.io + names: + kind: IdpClient + listKind: IdpClientList + plural: idpclients + shortNames: + - client + singular: idpclient + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IdpClient defines an OIDC client configuration. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the resource. + properties: + applicationType: + description: ApplicationType is the OAuth application type (e.g., + "web", "native"). + type: string + clientId: + description: |- + ClientId is the OAuth client identifier (e.g., "web-ui", "kantra"). + This is used as the natural key for reconciliation. + type: string + clientSecret: + description: |- + ClientSecret references a Kubernetes Secret containing the OAuth client secret. + The Secret must have a key named "clientSecret". + This is optional - public clients (e.g., native apps) may not require a secret. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + grants: + description: Grants are the OAuth grant types supported by this client. + items: + type: string + type: array + id: + description: |- + ID is the database ID for the seeded client. + Must be less than 1000 (reserved range for seeded clients). + maximum: 999 + minimum: 1 + type: integer + redirectURIs: + description: RedirectURIs are the redirect URIs for OAuth flows. + items: + type: string + type: array + scopes: + description: Scopes are the OAuth scopes requested by this client. + items: + type: string + type: array + required: + - applicationType + - clientId + - grants + - id + - scopes + type: object + status: + description: Status defines the observed state of the resource. + properties: + conditions: + description: Resource conditions. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: The most recent generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/tackle.konveyor.io_ldapproviders.yaml b/bundle/manifests/tackle.konveyor.io_ldapproviders.yaml new file mode 100644 index 00000000..3c10a2d4 --- /dev/null +++ b/bundle/manifests/tackle.konveyor.io_ldapproviders.yaml @@ -0,0 +1,239 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.0 + creationTimestamp: null + name: ldapproviders.tackle.konveyor.io +spec: + group: tackle.konveyor.io + names: + kind: LdapProvider + listKind: LdapProviderList + plural: ldapproviders + shortNames: + - ldap + singular: ldapprovider + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: LdapProvider defines LDAP authentication and authorization settings. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the resource. + properties: + baseDN: + description: Base DN for LDAP searches (e.g., dc=example,dc=com). + type: string + bindDN: + description: Service account bind DN for LDAP authentication. + type: string + groupFilter: + description: Custom group search filter (optional, defaults based + on Kind). + type: string + hasMemberOf: + description: Use memberOf attribute for group membership (faster if + available). + type: boolean + kind: + description: LDAP kind (ACTIVEDIRECTORY, AD, or blank for standard + LDAP). + type: string + name: + description: Provider name. + type: string + password: + description: Password reference for service account authentication. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + roleMappings: + description: Role mappings from LDAP groups to application roles. + items: + description: RoleMapping defines pattern matching for LDAP groups + to roles. + properties: + and: + description: And patterns (AND condition). + items: + type: string + type: array + any: + description: Any patterns (OR condition). + items: + type: string + type: array + roles: + description: Role name to assign when matched. + items: + type: string + type: array + required: + - roles + type: object + type: array + tls: + description: TLS connection settings. + properties: + ca: + description: |- + CA is a PEM-encoded CA certificate for validating the server certificate. + Use when the server uses a certificate signed by an internal/private CA. + type: string + insecure: + description: |- + Insecure skips server certificate verification. + Use only for development/testing with self-signed certificates. + type: boolean + type: object + url: + description: LDAP server URL (e.g., ldap://ldap.example.com:389). + type: string + userFilter: + description: Custom user search filter (optional, defaults based on + Kind). + type: string + required: + - baseDN + - bindDN + - name + - password + - roleMappings + - url + type: object + status: + description: Status defines the observed state of the resource. + properties: + conditions: + description: Resource conditions. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: The most recent generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/helm/templates/crds/tackle.konveyor.io_identityproviders.yaml b/helm/templates/crds/tackle.konveyor.io_identityproviders.yaml new file mode 100644 index 00000000..37133e5a --- /dev/null +++ b/helm/templates/crds/tackle.konveyor.io_identityproviders.yaml @@ -0,0 +1,196 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.0 + name: identityproviders.tackle.konveyor.io +spec: + group: tackle.konveyor.io + names: + kind: IdentityProvider + listKind: IdentityProviderList + plural: identityproviders + shortNames: + - idp + singular: identityprovider + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IdentityProvider defines external IDP federation settings. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the resource. + properties: + clientId: + description: Client ID. + type: string + clientSecret: + description: Client secret reference (optional for public clients). + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + issuer: + description: Issuer URL. + type: string + name: + description: Provider name. + type: string + redirectURI: + description: Redirect URI. + type: string + scopes: + description: OAuth scopes (optional, provider injects defaults if + empty). + items: + type: string + type: array + tls: + description: TLS connection settings. + properties: + ca: + description: |- + CA is a PEM-encoded CA certificate for validating the server certificate. + Use when the server uses a certificate signed by an internal/private CA. + type: string + insecure: + description: |- + Insecure skips server certificate verification. + Use only for development/testing with self-signed certificates. + type: boolean + type: object + required: + - clientId + - issuer + - name + - redirectURI + type: object + status: + description: Status defines the observed state of the resource. + properties: + conditions: + description: Resource conditions. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: The most recent generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/templates/crds/tackle.konveyor.io_idpclients.yaml b/helm/templates/crds/tackle.konveyor.io_idpclients.yaml new file mode 100644 index 00000000..2ccc2053 --- /dev/null +++ b/helm/templates/crds/tackle.konveyor.io_idpclients.yaml @@ -0,0 +1,199 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.0 + name: idpclients.tackle.konveyor.io +spec: + group: tackle.konveyor.io + names: + kind: IdpClient + listKind: IdpClientList + plural: idpclients + shortNames: + - client + singular: idpclient + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IdpClient defines an OIDC client configuration. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the resource. + properties: + applicationType: + description: ApplicationType is the OAuth application type (e.g., + "web", "native"). + type: string + clientId: + description: |- + ClientId is the OAuth client identifier (e.g., "web-ui", "kantra"). + This is used as the natural key for reconciliation. + type: string + clientSecret: + description: |- + ClientSecret references a Kubernetes Secret containing the OAuth client secret. + The Secret must have a key named "clientSecret". + This is optional - public clients (e.g., native apps) may not require a secret. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + grants: + description: Grants are the OAuth grant types supported by this client. + items: + type: string + type: array + id: + description: |- + ID is the database ID for the seeded client. + Must be less than 1000 (reserved range for seeded clients). + maximum: 999 + minimum: 1 + type: integer + redirectURIs: + description: RedirectURIs are the redirect URIs for OAuth flows. + items: + type: string + type: array + scopes: + description: Scopes are the OAuth scopes requested by this client. + items: + type: string + type: array + required: + - applicationType + - clientId + - grants + - id + - scopes + type: object + status: + description: Status defines the observed state of the resource. + properties: + conditions: + description: Resource conditions. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: The most recent generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/templates/crds/tackle.konveyor.io_ldapproviders.yaml b/helm/templates/crds/tackle.konveyor.io_ldapproviders.yaml new file mode 100644 index 00000000..53a80934 --- /dev/null +++ b/helm/templates/crds/tackle.konveyor.io_ldapproviders.yaml @@ -0,0 +1,233 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.0 + name: ldapproviders.tackle.konveyor.io +spec: + group: tackle.konveyor.io + names: + kind: LdapProvider + listKind: LdapProviderList + plural: ldapproviders + shortNames: + - ldap + singular: ldapprovider + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: LdapProvider defines LDAP authentication and authorization settings. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of the resource. + properties: + baseDN: + description: Base DN for LDAP searches (e.g., dc=example,dc=com). + type: string + bindDN: + description: Service account bind DN for LDAP authentication. + type: string + groupFilter: + description: Custom group search filter (optional, defaults based + on Kind). + type: string + hasMemberOf: + description: Use memberOf attribute for group membership (faster if + available). + type: boolean + kind: + description: LDAP kind (ACTIVEDIRECTORY, AD, or blank for standard + LDAP). + type: string + name: + description: Provider name. + type: string + password: + description: Password reference for service account authentication. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + roleMappings: + description: Role mappings from LDAP groups to application roles. + items: + description: RoleMapping defines pattern matching for LDAP groups + to roles. + properties: + and: + description: And patterns (AND condition). + items: + type: string + type: array + any: + description: Any patterns (OR condition). + items: + type: string + type: array + roles: + description: Role name to assign when matched. + items: + type: string + type: array + required: + - roles + type: object + type: array + tls: + description: TLS connection settings. + properties: + ca: + description: |- + CA is a PEM-encoded CA certificate for validating the server certificate. + Use when the server uses a certificate signed by an internal/private CA. + type: string + insecure: + description: |- + Insecure skips server certificate verification. + Use only for development/testing with self-signed certificates. + type: boolean + type: object + url: + description: LDAP server URL (e.g., ldap://ldap.example.com:389). + type: string + userFilter: + description: Custom user search filter (optional, defaults based on + Kind). + type: string + required: + - baseDN + - bindDN + - name + - password + - roleMappings + - url + type: object + status: + description: Status defines the observed state of the resource. + properties: + conditions: + description: Resource conditions. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: The most recent generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index ee7b708b..0192ae0c 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -35,8 +35,6 @@ spec: value: konveyor - name: VERSION value: {{ .Values.version }} - - name: RELATED_IMAGE_OAUTH_PROXY - value: {{ .Values.images.oauth_proxy }} - name: RELATED_IMAGE_TACKLE_HUB value: {{ .Values.images.tackle_hub }} - name: RELATED_IMAGE_TACKLE_POSTGRES diff --git a/helm/templates/rbac/role.yaml b/helm/templates/rbac/role.yaml index c1090204..4b6cd021 100644 --- a/helm/templates/rbac/role.yaml +++ b/helm/templates/rbac/role.yaml @@ -98,6 +98,9 @@ rules: - tackles/finalizers - addons - extensions + - identityproviders + - idpclients + - ldapproviders - tasks - schemas verbs: diff --git a/helm/values.yaml b/helm/values.yaml index cd306cff..e9812365 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -12,7 +12,6 @@ csv: images: operator: quay.io/konveyor/tackle2-operator:latest - oauth_proxy: quay.io/openshift/origin-oauth-proxy:latest tackle_hub: quay.io/konveyor/tackle2-hub:latest tackle_postgres: quay.io/sclorg/postgresql-15-c9s:latest keycloak_sso: quay.io/keycloak/keycloak:26.1 diff --git a/roles/tackle/defaults/main.yml b/roles/tackle/defaults/main.yml index 50fa5337..e8301ecc 100644 --- a/roles/tackle/defaults/main.yml +++ b/roles/tackle/defaults/main.yml @@ -9,7 +9,6 @@ app_version: "{{ lookup('env', 'VERSION') }}" # Feature defaults feature_auth_required: "{{ false if app_profile == 'konveyor' else true }}" -feature_auth_type: keycloak feature_isolate_namespace: true feature_analysis_archiver: true feature_discovery: true @@ -83,58 +82,26 @@ pathfinder_component_name: "pathfinder" pathfinder_service_name: "{{ app_name }}-{{ pathfinder_component_name }}" pathfinder_deployment_name: "{{ pathfinder_service_name }}" -keycloak_database_image_fqin: "{{ lookup('env', 'RELATED_IMAGE_TACKLE_POSTGRES') }}" -keycloak_database_name: "keycloak" -keycloak_database_component_name: "postgresql" -keycloak_database_service_name: "{{ app_name }}-{{ keycloak_database_name }}-{{ keycloak_database_component_name }}" -keycloak_database_service_k8s_resource_name: "{{ app_name }}-kcpgsql" -keycloak_database_secret_name: "{{ keycloak_database_service_name }}" -keycloak_database_deployment_name: "{{ keycloak_database_service_name }}" -keycloak_database_deployment_strategy: "Recreate" -keycloak_database_deployment_replicas: "1" -keycloak_database_container_name: "{{ keycloak_database_service_name }}" -keycloak_database_container_limits_cpu: "500m" -keycloak_database_container_limits_memory: "800Mi" -keycloak_database_container_requests_cpu: "100m" -keycloak_database_container_requests_memory: "350Mi" -keycloak_database_data_volume_name: "{{ keycloak_database_service_name }}-database" -keycloak_database_data_volume_size: "1Gi" -keycloak_database_data_volume_path: "/var/lib/pgsql" -keycloak_database_data_volume_claim_name: "{{ keycloak_database_service_name }}-{{ keycloak_database_db_version }}-volume-claim" -keycloak_database_db_name: "keycloak_db" -keycloak_database_db_name_b64: "{{ keycloak_database_db_name | b64encode }}" -keycloak_database_db_version: "15" - -keycloak_sso_image_fqin: "{{ lookup('env', 'RELATED_IMAGE_KEYCLOAK_SSO') }}" -keycloak_sso_name: "keycloak" -keycloak_sso_component_name: "{{ 'rhbk' if app_profile == 'mta' else 'sso' }}" -keycloak_sso_service_name: "{{ app_name }}-{{ keycloak_sso_name }}-{{ keycloak_sso_component_name }}" -keycloak_sso_configmap_name: "{{ keycloak_sso_service_name }}" -keycloak_sso_secret_name: "{{ keycloak_sso_service_name }}" -keycloak_sso_deployment_name: "{{ keycloak_sso_service_name }}" -keycloak_sso_deployment_strategy: "Recreate" -keycloak_sso_deployment_replicas: "1" -keycloak_sso_container_name: "{{ keycloak_sso_service_name }}" -keycloak_sso_container_limits_cpu: "1000m" -keycloak_sso_container_limits_memory: "2Gi" -keycloak_sso_container_requests_cpu: "300m" -keycloak_sso_container_requests_memory: "600Mi" -keycloak_sso_liveness_init_delay: "60" -keycloak_sso_readiness_init_delay: "60" -keycloak_sso_admin_username: "admin" -keycloak_sso_admin_username_b64: "{{ keycloak_sso_admin_username | b64encode }}" -keycloak_sso_java_opts: "-Dcom.redhat.fips=false" +# Deprecated Keycloak reference variables (for backwards compatibility only) +# Operator no longer deploys Keycloak - users must deploy and configure their own +# These variables exist only for explicit external Keycloak configuration + keycloak_sso_realm: "{{ app_name }}" -keycloak_sso_req_passwd_update: true keycloak_sso_client_id: "{{ app_name }}-ui" -keycloak_api_audience: "konveyor-api" -keycloak_sso_tls_enabled: "{{ true if openshift_cluster | bool else false }}" -keycloak_sso_tls_secret_name: "{{ keycloak_sso_service_name }}-serving-cert" -keycloak_sso_port: "{{ '8443' if keycloak_sso_tls_enabled | bool else '8080' }}" -keycloak_sso_proto: "{{ 'https' if keycloak_sso_tls_enabled | bool else 'http' }}" -keycloak_sso_url: "{{ keycloak_sso_proto }}://{{ keycloak_sso_service_name }}.{{ app_namespace }}.svc:{{ keycloak_sso_port }}" -keycloak_sso_hostname: "" -keycloak_sso_hostname_backchannel_dynamic: false +keycloak_sso_url: "" # Set explicitly for external Keycloak +rhbk_url: "" # Set explicitly for external RHBK + +# Hub OIDC configuration +app_base_url: "" # Set at runtime from UI route/ingress hostname (external public URL) +hub_oidc_issuer: "" # Set at runtime from app_base_url + /oidc (external public URL) + +# UI OIDC client configuration +ui_oidc_client_id: "web-ui" + +# Federated identity provider configuration (for Keycloak or other OIDC providers) +# Set at runtime based on detection or explicit configuration +federated_idp_issuer: "" +federated_idp_client_id: "{{ keycloak_sso_client_id }}" ui_image_fqin: "{{ lookup('env', 'RELATED_IMAGE_TACKLE_UI') }}" ui_component_name: "ui" @@ -164,13 +131,6 @@ ui_route_tls_insecure_termination_policy: "Redirect" # the default value for the ingress controller you are using # ui_ingress_path_type: - -oauth_provider: openshift -oauth_default_openshift_sar: --openshift-sar={"namespace":"{{ app_namespace }}","resource":"services","resourceName":"{{ ui_service_name }}","verb":"get"} -oauth_access_rule: "{{ oauth_default_openshift_sar if oauth_provider == 'openshift' }}" -oauth_image_fqin: "{{ lookup('env', 'RELATED_IMAGE_OAUTH_PROXY') }}" -oauth_ssl_port: 8443 - admin_name: "admin" analyzer_fqin: "{{ lookup('env', 'RELATED_IMAGE_ADDON_ANALYZER') }}" @@ -262,27 +222,6 @@ cache_data_volume_claim_mode: "ReadWriteMany" cache_mount_path: "/cache" rwx_supported: false -# RH-SSO specific -rhsso_name: "rhsso" -rhsso_service_name: "{{ app_name }}-{{ rhsso_name }}" -rhsso_secret_name: "credential-{{ rhsso_service_name }}" -rhsso_api_version: "keycloak.org/v1alpha1" -rhsso_external_access: false -rhsso_tls_enabled: true -rhsso_port: "{{ '8443' if rhsso_tls_enabled | bool else '8080' }}" -rhsso_proto: "{{ 'https' if rhsso_tls_enabled | bool else 'http' }}" -rhsso_url: "{{ rhsso_proto }}://keycloak.{{ app_namespace }}.svc:{{ rhsso_port }}" - -# RHBK Specific -rhbk_name: "rhbk" -rhbk_service_name: "{{ app_name }}-{{ rhbk_name }}" -rhbk_api_version: "k8s.keycloak.org/v2alpha1" -rhbk_tls_enabled: "{{ true if openshift_cluster | bool else false }}" -rhbk_tls_secret_name: "{{ rhbk_service_name }}-serving-cert" -rhbk_port: "{{ '8443' if rhsso_tls_enabled | bool else '8080' }}" -rhbk_proto: "{{ 'https' if rhsso_tls_enabled | bool else 'http' }}" -rhbk_url: "{{ rhsso_proto }}://{{ rhbk_service_name }}-service.{{ app_namespace }}.svc:{{ rhsso_port }}" - # Kai-related variables # experimental_deploy_kai is deprecated in favor of kai_solution_server_enabled # But left for the sake of backwards compatibility for now @@ -348,8 +287,10 @@ kai_database_address: kai-db.{{ app_namespace }}.svc kai_llm_proxy_enabled: false kai_llm_proxy_image_fqin: "{{ lookup('env', 'RELATED_IMAGE_LIGHTSPEED_STACK') | default('quay.io/lightspeed-core/lightspeed-stack:latest', true) }}" -# Internal URL for the LLM proxy service (used by UI reverse proxy) -kai_llm_proxy_url: "http://llm-proxy.{{ app_namespace }}.svc.cluster.local:8321" +# Internal URL the hub uses to reach llm-proxy. The hub exposes it externally +# via /services/llm-proxy after authenticating the bearer token; only the hub +# should call this URL directly. +kai_llm_proxy_internal_url: "http://llm-proxy.{{ app_namespace }}.svc:8321" # Client configuration ConfigMap name kai_llm_proxy_client_config_name: "llm-proxy-client" diff --git a/roles/tackle/tasks/main.yml b/roles/tackle/tasks/main.yml index d908b3f9..90389148 100644 --- a/roles/tackle/tasks/main.yml +++ b/roles/tackle/tasks/main.yml @@ -53,57 +53,9 @@ state: present definition: "{{ lookup('template', 'configmap-trusted-ca.yml.j2') }}" -- when: - - feature_auth_required|bool - - feature_auth_type == "oauth" - block: - - name: "Check if Cookie Secret already exists" - k8s_info: - api_version: v1 - kind: Secret - name: cookie-secret - namespace: "{{ app_namespace }}" - register: cookie_secret - - - name: "Generate Cookie Secret" - set_fact: - new_cookie_secret: "{{ lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=32') }}" - when: (cookie_secret.resources | length) == 0 - - - name: "Create Cookie Secret" - k8s: - state: present - definition: "{{ lookup('template', 'secret-cookie-secret.yml.j2') }}" - when: (cookie_secret.resources | length) == 0 - - - name: "Retrieve Cookie Secret" - k8s_info: - api_version: v1 - kind: Secret - name: cookie-secret - namespace: "{{ app_namespace }}" - register: cookie_secret - - - name: "Set Cookie Secret" - set_fact: - cookie_secret_data: "{{ cookie_secret.resources[0].data['cookie-secret'] | b64decode }}" - - - name: "Retrieve Oauth Client Secret if it exists" - k8s_info: - api_version: v1 - kind: Secret - name: oauth-client-secret - namespace: "{{ app_namespace }}" - register: oauth_client_secret_status - - - name: "Set Oauth Client Secret" - set_fact: - oauth_client_secret: "{{ oauth_client_secret_status.resources[0].data['client-secret'] | b64decode }}" - when: (oauth_client_secret_status.resources | length) > 0 - -- when: - - feature_auth_required|bool - - feature_auth_type == "keycloak" +# Keycloak deployment is disabled - operator no longer deploys or maintains Keycloak +# Users should configure federated_idp_issuer to point to their Keycloak if needed +- when: false block: - name: "Setup Keycloak PostgreSQL PersistentVolumeClaim" k8s: @@ -412,10 +364,9 @@ state: present definition: "{{ lookup('template', 'deployment-keycloak-sso.yml.j2') }}" -- when: - - feature_auth_required|bool - - feature_auth_type == "keycloak" - - app_profile == "mta" +# RHBK deployment is disabled - operator no longer deploys or maintains RHBK/Keycloak +# Users should configure federated_idp_issuer to point to their RHBK if needed +- when: false block: - name: "Check for existing RHSSO Keycloak CR" k8s_info: @@ -588,37 +539,72 @@ namespace: "{{ app_namespace }}" register: hub_secret_status -- when: (hub_secret_status.resources | length) == 0 +- name: "Detect operator-deployed Keycloak variants for federated authentication" block: - - name: "Generate Hub random AES passphrase" - set_fact: - hub_aes_passphrase: "{{ lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=32') }}" + - name: "Check for standalone Keycloak service" + kubernetes.core.k8s_info: + api_version: v1 + kind: Service + namespace: "{{ app_namespace }}" + label_selectors: + - "app.kubernetes.io/part-of={{ app_name }}" + - "app.kubernetes.io/component=sso" + register: standalone_keycloak_svc - - name: "Encode Hub AES passphrase" - set_fact: - hub_aes_passphrase_b64: "{{ hub_aes_passphrase | b64encode }}" + - name: "Check for RHSSO Keycloak CR" + kubernetes.core.k8s_info: + api_version: keycloak.org/v1alpha1 + kind: Keycloak + namespace: "{{ app_namespace }}" + label_selectors: + - "app={{ app_name }}-rhsso" + register: rhsso_keycloak_cr + when: + - app_profile == 'mta' - - name: "Generate Hub addon token" - set_fact: - hub_addon_token: "{{ lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=32') }}" + - name: "Check for RHBK Keycloak CR" + kubernetes.core.k8s_info: + api_version: k8s.keycloak.org/v2alpha1 + kind: Keycloak + namespace: "{{ app_namespace }}" + name: "{{ app_name }}-keycloak" + register: rhbk_keycloak_cr + when: + - app_profile == 'mta' - - name: "Encode Hub addon token" + - name: "Set federated IDP issuer based on detection or explicit config" set_fact: - hub_addon_token_b64: "{{ hub_addon_token | b64encode }}" - - - name: "Setup Hub Secret" - k8s: - state: present - definition: "{{ lookup('template', 'secret-hub.yml.j2') }}" + federated_idp_issuer: >- + {% if keycloak_sso_url | length > 0 %} + {{ keycloak_sso_url }}/realms/{{ keycloak_sso_realm }} + {% elif rhbk_url | length > 0 %} + {{ rhbk_url }}/realms/{{ keycloak_sso_realm }} + {% elif standalone_keycloak_svc.resources | length > 0 %} + http://{{ standalone_keycloak_svc.resources[0].metadata.name }}:8080/realms/{{ keycloak_sso_realm }} + {% elif rhsso_keycloak_cr is defined and rhsso_keycloak_cr.resources | length > 0 %} + http://keycloak.{{ app_namespace }}.svc:8080/realms/{{ keycloak_sso_realm }} + {% elif rhbk_keycloak_cr is defined and rhbk_keycloak_cr.resources | length > 0 %} + https://{{ app_name }}-keycloak-service.{{ app_namespace }}.svc:8443/realms/{{ keycloak_sso_realm }} + {% else %} + + {% endif %} + +- name: "Set Hub secrets from existing secret or generate new ones" + set_fact: + hub_aes_passphrase: "{{ hub_secret_status.resources[0].data.passphrase | b64decode if (hub_secret_status.resources | length) > 0 else lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=32') }}" + hub_addon_token: "{{ hub_secret_status.resources[0].data.addon_token | b64decode if (hub_secret_status.resources | length) > 0 else lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=32') }}" + hub_apikey_secret: "{{ hub_secret_status.resources[0].data.apikey_secret | b64decode if (hub_secret_status.resources | length) > 0 and 'apikey_secret' in hub_secret_status.resources[0].data else lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits length=32') }}" -- name: "Look up Keycloak DB Secret for Hashing" +- name: "Set Hub secrets base64 encoded" set_fact: - keycloak_db_secret: - env: "{{ lookup('template', 'secret-keycloak-db.yml.j2') | from_yaml }}" - when: - - feature_auth_required|bool - - feature_auth_type == "keycloak" - - app_profile == "mta" + hub_aes_passphrase_b64: "{{ hub_aes_passphrase | b64encode }}" + hub_addon_token_b64: "{{ hub_addon_token | b64encode }}" + hub_apikey_secret_b64: "{{ hub_apikey_secret | b64encode }}" + +- name: "Setup Hub Secret" + k8s: + state: present + definition: "{{ lookup('template', 'secret-hub.yml.j2') }}" # Create all the neccessary CR's before the hub deployment is created - name: "Remove Admin Addon CR" @@ -730,6 +716,102 @@ definition: "{{ lookup('template', 'route-ui.yml.j2') }}" when: openshift_cluster|bool +- name: "Get UI Route hostname for Hub OIDC issuer" + kubernetes.core.k8s_info: + api_version: route.openshift.io/v1 + kind: Route + namespace: "{{ app_namespace }}" + name: "{{ ui_route_name }}" + register: ui_route_info + when: + - openshift_cluster|bool + +- name: "Set app base URL from UI Route" + set_fact: + app_base_url: "https://{{ ui_route_info.resources[0].spec.host }}" + when: + - openshift_cluster|bool + - ui_route_info.resources | length > 0 + +- name: "Set Hub OIDC issuer URL from app base URL" + set_fact: + hub_oidc_issuer: "{{ app_base_url }}/oidc" + when: + - openshift_cluster|bool + - ui_route_info.resources | length > 0 + +- name: "Get UI Ingress hostname for Hub OIDC issuer" + kubernetes.core.k8s_info: + api_version: networking.k8s.io/v1 + kind: Ingress + namespace: "{{ app_namespace }}" + name: "{{ ui_ingress_name }}" + register: ui_ingress_info + when: + - not openshift_cluster|bool + +- name: "Set app base URL from UI Ingress" + set_fact: + app_base_url: "https://{{ ui_ingress_info.resources[0].spec.rules[0].host }}" + when: + - not openshift_cluster|bool + - ui_ingress_info.resources | length > 0 + +- name: "Set Hub OIDC issuer URL from app base URL" + set_fact: + hub_oidc_issuer: "{{ app_base_url }}/oidc" + when: + - not openshift_cluster|bool + - ui_ingress_info.resources | length > 0 + +- name: "Create IdentityProvider CR for federated authentication" + k8s: + state: present + definition: "{{ lookup('template', 'customresource-idp.yml.j2') }}" + when: + - feature_auth_required|bool + - federated_idp_issuer is defined + - federated_idp_issuer | length > 0 + - hub_oidc_issuer is defined + - hub_oidc_issuer | length > 0 + +- name: "Notify about federated identity provider configuration" + debug: + msg: | + Hub OIDC is now configured to federate authentication to an external identity provider. + Federated IDP Issuer: {{ federated_idp_issuer }} + Using existing client: {{ federated_idp_client_id }} + Hub OIDC Issuer: {{ hub_oidc_issuer }} + Redirect URI: {{ hub_oidc_issuer }}/callback + + NOTE: Ensure the existing '{{ federated_idp_client_id }}' client in the federated identity provider has the redirect URI '{{ hub_oidc_issuer }}/callback' configured. + when: + - feature_auth_required|bool + - federated_idp_issuer is defined + - federated_idp_issuer | length > 0 + - hub_oidc_issuer is defined + - hub_oidc_issuer | length > 0 + +- name: "Verify app_base_url is available for IdpClient CRs" + fail: + msg: | + Cannot create IdpClient CRs: app_base_url is not set. + The UI Route (OpenShift) or Ingress (Kubernetes) must be created and available + before IdpClient custom resources can be generated with valid redirect URIs. + Check that the UI deployment succeeded and the Route/Ingress exists. + when: + - feature_auth_required|bool + - (app_base_url is not defined or app_base_url | length == 0) + +- name: "Create IdpClient CRs" + k8s: + state: present + definition: "{{ lookup('template', 'customresource-idpclient.yml.j2') }}" + when: + - feature_auth_required|bool + - app_base_url is defined + - app_base_url | length > 0 + - name: "Check if Cache PersistentVolumeClaim exists" kubernetes.core.k8s_info: api_version: v1 @@ -782,7 +864,7 @@ when: feature_isolate_namespace|bool - when: - - not(feature_auth_required|bool) or not(feature_auth_type == "keycloak") + - not(feature_auth_required|bool) block: - name: "Deprovision RHSSO Keycloak CR" diff --git a/roles/tackle/templates/customresource-idp.yml.j2 b/roles/tackle/templates/customresource-idp.yml.j2 new file mode 100644 index 00000000..f67ce537 --- /dev/null +++ b/roles/tackle/templates/customresource-idp.yml.j2 @@ -0,0 +1,22 @@ +--- +apiVersion: tackle.konveyor.io/v1alpha1 +kind: IdentityProvider +metadata: + name: {{ app_name }}-federated-idp + namespace: {{ app_namespace }} + labels: + app.kubernetes.io/name: {{ app_name }}-federated-idp + app.kubernetes.io/component: identityprovider + app.kubernetes.io/part-of: {{ app_name }} + app: {{ app_name }} +spec: + name: federated-idp + issuer: "{{ federated_idp_issuer }}" + clientId: "{{ federated_idp_client_id }}" + redirectURI: "{{ hub_oidc_issuer }}/callback" + scopes: + - openid + - profile + - email + tls: + insecure: true diff --git a/roles/tackle/templates/customresource-idpclient.yml.j2 b/roles/tackle/templates/customresource-idpclient.yml.j2 new file mode 100644 index 00000000..c3ae3b38 --- /dev/null +++ b/roles/tackle/templates/customresource-idpclient.yml.j2 @@ -0,0 +1,82 @@ +--- +# web-ui client +apiVersion: tackle.konveyor.io/v1alpha1 +kind: IdpClient +metadata: + name: {{ app_name }}-web-ui + namespace: {{ app_namespace }} + labels: + app.kubernetes.io/name: {{ app_name }}-web-ui + app.kubernetes.io/component: idpclient + app.kubernetes.io/part-of: {{ app_name }} + app: {{ app_name }} +spec: + id: 1 + clientId: web-ui + applicationType: web + grants: + - urn:ietf:params:oauth:grant-type:jwt-bearer + - authorization_code + - refresh_token + redirectURIs: + - {{ app_base_url }} + scopes: + - offline_access + - openid + - profile + - email + +--- +# kantra client (public client - no secret needed) +apiVersion: tackle.konveyor.io/v1alpha1 +kind: IdpClient +metadata: + name: {{ app_name }}-kantra + namespace: {{ app_namespace }} + labels: + app.kubernetes.io/name: {{ app_name }}-kantra + app.kubernetes.io/component: idpclient + app.kubernetes.io/part-of: {{ app_name }} + app: {{ app_name }} +spec: + id: 2 + clientId: kantra + applicationType: native + grants: + - urn:ietf:params:oauth:grant-type:device_code + - authorization_code + - refresh_token + scopes: + - offline_access + - openid + - profile + - email + +--- +# kai-ide client (public client - no secret needed) +apiVersion: tackle.konveyor.io/v1alpha1 +kind: IdpClient +metadata: + name: {{ app_name }}-kai-ide + namespace: {{ app_namespace }} + labels: + app.kubernetes.io/name: {{ app_name }}-kai-ide + app.kubernetes.io/component: idpclient + app.kubernetes.io/part-of: {{ app_name }} + app: {{ app_name }} +spec: + id: 3 + clientId: kai-ide + applicationType: native + grants: + - urn:ietf:params:oauth:grant-type:jwt-bearer + - authorization_code + - refresh_token + redirectURIs: + - vscode://konveyor.konveyor-core/auth + - http://127.0.0.1/callback + scopes: + - offline_access + - openid + - profile + - email diff --git a/roles/tackle/templates/customresource-rhbk-keycloak.yml.j2 b/roles/tackle/templates/customresource-rhbk-keycloak.yml.j2 deleted file mode 100644 index fb882a88..00000000 --- a/roles/tackle/templates/customresource-rhbk-keycloak.yml.j2 +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: {{ rhbk_api_version }} -kind: Keycloak -metadata: - name: {{ app_name }}-{{ rhbk_name }} - namespace: {{ app_namespace }} -spec: - instances: {{ rhbk_instances }} - ingress: - enabled: false - db: - vendor: postgres - database: {{ keycloak_database_db_name }} - host: {{ keycloak_database_service_k8s_resource_name }} - usernameSecret: - name: keycloak-db-secret - key: POSTGRES_USERNAME - passwordSecret: - name: keycloak-db-secret - key: POSTGRES_PASSWORD - proxy: - headers: xforwarded - resources: - limits: - cpu: {{ keycloak_sso_container_limits_cpu }} - memory: {{ keycloak_sso_container_limits_memory }} - requests: - cpu: {{ keycloak_sso_container_requests_cpu }} - memory: {{ keycloak_sso_container_requests_memory }} - http: - tlsSecret: {{ rhbk_tls_secret_name }} - hostname: - strict: false - additionalOptions: - - name: http-relative-path - value: /auth - bootstrapAdmin: - user: - secret: {{ keycloak_sso_secret_name }} - diff --git a/roles/tackle/templates/customresource-rhsso-keycloak.yml.j2 b/roles/tackle/templates/customresource-rhsso-keycloak.yml.j2 deleted file mode 100644 index 7d154c5d..00000000 --- a/roles/tackle/templates/customresource-rhsso-keycloak.yml.j2 +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: {{ rhsso_api_version }} -kind: Keycloak -metadata: - name: {{ rhsso_service_name }} - namespace: {{ app_namespace }} - labels: - app: {{ rhsso_service_name }} -spec: - instances: {{ rhsso_instances | default('1') }} - externalDatabase: - enabled: true - externalAccess: - enabled: {{ rhsso_external_access }} diff --git a/roles/tackle/templates/deployment-hub.yml.j2 b/roles/tackle/templates/deployment-hub.yml.j2 index c007d603..2ec77a37 100644 --- a/roles/tackle/templates/deployment-hub.yml.j2 +++ b/roles/tackle/templates/deployment-hub.yml.j2 @@ -8,17 +8,6 @@ metadata: app.kubernetes.io/name: {{ hub_service_name }} app.kubernetes.io/component: {{ hub_component_name }} app.kubernetes.io/part-of: {{ app_name }} - annotations: - app.openshift.io/connects-to: >- - [ -{% if feature_auth_required|bool and feature_auth_type == "keycloak" %} -{% if app_profile == 'konveyor' %} - { "apiVersion": "apps/v1", "kind": "Deployment", "name": "{{ keycloak_sso_deployment_name }}" }, -{% elif app_profile == 'mta' %} - { "apiVersion": "apps/v1", "kind": "StatefulSet", "name": "keycloak" }, -{% endif %} -{% endif %} - ] spec: replicas: {{ hub_deployment_replicas }} selector: @@ -38,11 +27,6 @@ spec: app.kubernetes.io/part-of: {{ app_name }} app: {{ app_name }} role: {{ hub_service_name }} -{% if feature_auth_required|bool %} -{% if app_profile == 'mta' and feature_auth_type == "keycloak" %} - keycloak_db_secret_name: {{ keycloak_db_secret.env | k8s_config_resource_name }} -{% endif %} -{% endif %} spec: serviceAccountName: {{ hub_serviceaccount_name }} containers: @@ -94,6 +78,11 @@ spec: secretKeyRef: name: "{{ hub_secret_name }}" key: addon_token + - name: APIKEY_SECRET + valueFrom: + secretKeyRef: + name: "{{ hub_secret_name }}" + key: apikey_secret - name: METRICS_ENABLED value: "{{ hub_metrics_enabled }}" - name: METRICS_PORT @@ -118,40 +107,10 @@ spec: - name: LOG_TASK value: "{{ hub_log_level_task }}" {% endif %} -{% if feature_auth_required|bool and feature_auth_type == "keycloak" %} - - name: AUTH_REQUIRED - value: "true" -{% else %} - name: AUTH_REQUIRED - value: "false" -{% endif %} -{% if feature_auth_required|bool and feature_auth_type == "keycloak" %} - - name: KEYCLOAK_REALM - value: "{{ keycloak_sso_realm }}" - - name: KEYCLOAK_CLIENT_ID - value: "{{ keycloak_sso_client_id }}" -{% if app_profile == 'mta' %} - - name: KEYCLOAK_HOST - value: "{{ rhbk_url }}" -{% else %} - - name: KEYCLOAK_HOST - value: "{{ keycloak_sso_url }}" -{% endif %} - - name: KEYCLOAK_ADMIN_USER - valueFrom: - secretKeyRef: - name: "{{ keycloak_sso_secret_name }}" - key: username - - name: KEYCLOAK_ADMIN_PASS - valueFrom: - secretKeyRef: - name: "{{ keycloak_sso_secret_name }}" - key: password - - name: KEYCLOAK_REQ_PASS_UPDATE - value: "{{ keycloak_sso_req_passwd_update|lower }}" - - name: KEYCLOAK_AUDIENCE - value: "{{ keycloak_api_audience }}" -{% endif %} + value: "{{ feature_auth_required | string | lower }}" + - name: OIDC_ISSUER + value: "{{ hub_oidc_issuer }}" - name: TASK_SA value: "{{ hub_task_sa }}" {% if hub_task_reap_created is defined %} @@ -210,6 +169,10 @@ spec: {% endif %} - name: KAI_URL value: "{{ kai_url }}" +{% if kai_llm_proxy_enabled|bool %} + - name: LLM_PROXY_URL + value: "{{ kai_llm_proxy_internal_url }}" +{% endif %} ports: - containerPort: {{ hub_port }} protocol: TCP diff --git a/roles/tackle/templates/deployment-keycloak-postgresql.yml.j2 b/roles/tackle/templates/deployment-keycloak-postgresql.yml.j2 deleted file mode 100644 index 768b509d..00000000 --- a/roles/tackle/templates/deployment-keycloak-postgresql.yml.j2 +++ /dev/null @@ -1,98 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ keycloak_database_deployment_name }}-{{ keycloak_database_db_version }} - namespace: {{ app_namespace }} - labels: - app.kubernetes.io/name: {{ keycloak_database_service_name }}-{{ keycloak_database_db_version }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - version: "{{ keycloak_database_db_version }}" -spec: - replicas: {{ keycloak_database_deployment_replicas }} - selector: - matchLabels: - app.kubernetes.io/name: {{ keycloak_database_service_name }}-{{ keycloak_database_db_version }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - version: "{{ keycloak_database_db_version }}" -{% if keycloak_database_deployment_strategy == 'Recreate' %} - strategy: - type: {{ keycloak_database_deployment_strategy }} -{% endif %} - template: - metadata: - labels: - app.kubernetes.io/name: {{ keycloak_database_service_name }}-{{ keycloak_database_db_version }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - app: {{ app_name }} - role: {{ keycloak_database_service_name }} - version: "{{ keycloak_database_db_version }}" - spec: - containers: - - name: {{ keycloak_database_container_name }} - image: "{{ keycloak_database_image_fqin }}" - imagePullPolicy: "{{ image_pull_policy }}" - env: - - name: POSTGRESQL_USER - valueFrom: - secretKeyRef: - name: {{ keycloak_database_secret_name }} - key: database-user - - name: POSTGRESQL_PASSWORD - valueFrom: - secretKeyRef: - name: {{ keycloak_database_secret_name }} - key: database-password - - name: POSTGRESQL_DATABASE - valueFrom: - secretKeyRef: - name: {{ keycloak_database_secret_name }} - key: database-name - ports: - - containerPort: 5432 - protocol: TCP - resources: - limits: - cpu: {{ keycloak_database_container_limits_cpu }} - memory: {{ keycloak_database_container_limits_memory }} - requests: - cpu: {{ keycloak_database_container_requests_cpu }} - memory: {{ keycloak_database_container_requests_memory }} - volumeMounts: - - name: {{ keycloak_database_data_volume_name }} - mountPath: {{ keycloak_database_data_volume_path }} - livenessProbe: - exec: - command: - - "/bin/sh" - - "-c" - - 'psql -U $POSTGRESQL_USER -d $POSTGRESQL_DATABASE -c ''SELECT 1'' ' - initialDelaySeconds: 60 - timeoutSeconds: 10 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - exec: - command: - - "/bin/sh" - - "-c" - - 'psql -U $POSTGRESQL_USER -d $POSTGRESQL_DATABASE -c ''SELECT 1'' ' - initialDelaySeconds: 10 - timeoutSeconds: 1 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - terminationMessagePath: "/dev/termination-log" - terminationMessagePolicy: File -{% if not openshift_cluster %} - securityContext: - fsGroup: 26 -{% endif %} - volumes: - - name: {{ keycloak_database_data_volume_name }} - persistentVolumeClaim: - claimName: {{ keycloak_database_data_volume_claim_name }} diff --git a/roles/tackle/templates/deployment-keycloak-sso.yml.j2 b/roles/tackle/templates/deployment-keycloak-sso.yml.j2 deleted file mode 100644 index ef4f4406..00000000 --- a/roles/tackle/templates/deployment-keycloak-sso.yml.j2 +++ /dev/null @@ -1,146 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ keycloak_sso_deployment_name }} - namespace: {{ app_namespace }} - labels: - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - annotations: - app.openshift.io/connects-to: >- - [ - { "apiVersion": "apps/v1", "kind": "Deployment", "name": "{{ keycloak_database_deployment_name }}" } - ] -spec: - replicas: {{ keycloak_sso_deployment_replicas }} - selector: - matchLabels: - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} -{% if keycloak_sso_deployment_strategy == 'Recreate' %} - strategy: - type: {{ keycloak_sso_deployment_strategy }} -{% endif %} - template: - metadata: - labels: - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - app: {{ app_name }} - role: {{ keycloak_sso_service_name }} - spec: - containers: - - name: {{ keycloak_sso_container_name }} - image: "{{ keycloak_sso_image_fqin }}" - args: - - -Djgroups.dns.query=mta-kc-discovery.openshift-mta - - --verbose - - start -{% if keycloak_sso_hostname %} - - --hostname={{ keycloak_sso_hostname }} -{% if keycloak_sso_hostname_backchannel_dynamic|bool %} - - --hostname-backchannel-dynamic=true -{% endif %} -{% endif %} - imagePullPolicy: "{{ image_pull_policy }}" - env: - - name: KC_BOOTSTRAP_ADMIN_USERNAME - valueFrom: - secretKeyRef: - name: {{ keycloak_sso_secret_name }} - key: username - - name: KC_BOOTSTRAP_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: {{ keycloak_sso_secret_name }} - key: password - - name: JAVA_OPTS - value: {{ keycloak_sso_java_opts }} - - name: PROXY_ADDRESS_FORWARDING - value: 'true' - - name: KC_DB - value: postgres - - name: KC_DB_URL - value: jdbc:postgresql://{{ keycloak_database_service_k8s_resource_name }}:5432/{{ keycloak_database_db_name }} - - name: KC_DB_USERNAME - valueFrom: - secretKeyRef: - name: {{ keycloak_database_secret_name }} - key: database-user - - name: KC_DB_PASSWORD - valueFrom: - secretKeyRef: - name: {{ keycloak_database_secret_name }} - key: database-password - - name: KC_HTTP_RELATIVE_PATH - value: /auth - - name: KC_PROXY_HEADERS - value: xforwarded -{% if keycloak_sso_tls_enabled|bool %} - - name: KC_HTTPS_CERTIFICATE_FILE - value: /service-crt/tls.crt - - name: KC_HTTPS_CERTIFICATE_KEY_FILE - value: /service-crt/tls.key -{% endif %} - - name: KC_HOSTNAME_STRICT - value: "false" - - name: KC_HTTP_ENABLED - value: "true" -{% if keycloak_sso_hostname %} - - name: KC_HOSTNAME - value: "{{ keycloak_sso_hostname }}" -{% if keycloak_sso_hostname_backchannel_dynamic|bool %} - - name: KC_HOSTNAME_BACKCHANNEL_DYNAMIC - value: "true" -{% endif %} -{% endif %} - ports: - - name: http - containerPort: 8080 - protocol: TCP - - name: https - containerPort: 8443 - protocol: TCP - resources: - limits: - cpu: {{ keycloak_sso_container_limits_cpu }} - memory: {{ keycloak_sso_container_limits_memory }} - requests: - cpu: {{ keycloak_sso_container_requests_cpu }} - memory: {{ keycloak_sso_container_requests_memory }} - livenessProbe: - httpGet: - path: / - port: {{ keycloak_sso_port }} - scheme: {{ keycloak_sso_proto|upper }} - initialDelaySeconds: {{ keycloak_sso_liveness_init_delay }} - timeoutSeconds: 1 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 20 - readinessProbe: - httpGet: - path: / - port: {{ keycloak_sso_port }} - scheme: {{ keycloak_sso_proto|upper }} - initialDelaySeconds: {{ keycloak_sso_readiness_init_delay }} - timeoutSeconds: 1 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 20 -{% if keycloak_sso_tls_enabled|bool %} - volumeMounts: - - mountPath: "/service-crt" - name: service-crt - readOnly: true -{% endif %} -{% if keycloak_sso_tls_enabled|bool %} - volumes: - - name: service-crt - secret: - secretName: {{ keycloak_sso_tls_secret_name }} -{% endif %} diff --git a/roles/tackle/templates/deployment-ui.yml.j2 b/roles/tackle/templates/deployment-ui.yml.j2 index 4aa1d2c9..25dd2617 100644 --- a/roles/tackle/templates/deployment-ui.yml.j2 +++ b/roles/tackle/templates/deployment-ui.yml.j2 @@ -11,11 +11,6 @@ metadata: annotations: app.openshift.io/connects-to: >- [ -{% if feature_auth_required|bool and feature_auth_type == "keycloak" and app_profile == 'konveyor' %} - { "apiVersion": "apps/v1", "kind": "Deployment", "name": "{{ keycloak_sso_deployment_name }}" }, -{% elif feature_auth_required|bool and feature_auth_type == "keycloak" and app_profile == 'mta' %} - { "apiVersion": "apps/v1", "kind": "StatefulSet", "name": "keycloak" }, -{% endif %} { "apiVersion": "apps/v1", "kind": "Deployment", "name": "{{ hub_deployment_name }}" } ] spec: @@ -35,51 +30,6 @@ spec: role: {{ ui_service_name }} spec: containers: -{% if feature_auth_required|bool and feature_auth_type == "oauth" %} - - args: - - --https-address=:{{ oauth_ssl_port }} - - --provider={{ oauth_provider }} - - --upstream=http://localhost:{{ ui_port }} - - --cookie-secret={{ cookie_secret_data }} -{% if oauth_provider == "openshift" %} - - --openshift-service-account={{ ui_serviceaccount_name }} - - --tls-cert=/etc/tls/private/tls.crt - - --tls-key=/etc/tls/private/tls.key -{% else %} - - --tls-cert-file=/etc/tls/private/tls.crt - - --tls-key-file=/etc/tls/private/tls.key -{% if oauth_email_domain is defined %} - - --email-domain={{ oauth_email_domain }} -{% endif %} -{% if oauth_client_id is defined %} - - --client-id={{ oauth_client_id }} -{% endif %} -{% if oauth_client_secret is defined %} - - --client-secret={{ oauth_client_secret }} -{% endif %} -{% endif %} -{% if oauth_access_rule != "" %} - - {{ oauth_access_rule }} -{% endif %} -{% if oauth_extra_opts is defined %} -{% for item in oauth_extra_opts %} - - {{ item }} -{% endfor %} -{% endif %} - image: "{{ oauth_image_fqin }}" - imagePullPolicy: "{{ image_pull_policy }}" - name: oauth-proxy - ports: - - containerPort: 8081 - name: public - protocol: TCP - resources: {} - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /etc/tls/private - name: {{ ui_tls_secret_name }} -{% endif %} - name: {{ ui_container_name }} image: "{{ ui_image_fqin }}" imagePullPolicy: "{{ image_pull_policy }}" @@ -94,28 +44,12 @@ spec: value: '{{ui_ingress_proxy_body_size}}' - name: TACKLE_HUB_URL value: "{{ hub_url }}" -{% if kai_llm_proxy_enabled|bool %} - - name: KAI_LLM_PROXY_URL - value: "{{ kai_llm_proxy_url }}" -{% endif %} -{% if feature_auth_required|bool and feature_auth_type == "keycloak" %} - name: AUTH_REQUIRED - value: "true" - - name: KEYCLOAK_REALM - value: {{ keycloak_sso_realm }} - - name: KEYCLOAK_CLIENT_ID - value: {{ keycloak_sso_client_id }} -{% if app_profile == 'mta' %} - - name: KEYCLOAK_SERVER_URL - value: {{ rhbk_url }} -{% else %} - - name: KEYCLOAK_SERVER_URL - value: {{ keycloak_sso_url }} -{% endif %} -{% else %} - - name: AUTH_REQUIRED - value: "false" -{% endif %} + value: "{{ feature_auth_required | string | lower }}" + - name: OIDC_ISSUER + value: "{{ hub_oidc_issuer }}" + - name: OIDC_CLIENT_ID + value: "{{ ui_oidc_client_id }}" - name: NODE_EXTRA_CA_CERTS value: {{ ui_node_extra_ca_certs }} {% if ui_tls_enabled|bool %} @@ -174,7 +108,7 @@ spec: {% endif %} serviceAccount: {{ ui_serviceaccount_name }} volumes: -{% if ui_tls_enabled|bool or (feature_auth_required|bool and feature_auth_type == "oauth") %} +{% if ui_tls_enabled|bool %} - name: {{ ui_tls_secret_name }} secret: secretName: {{ ui_tls_secret_name }} diff --git a/roles/tackle/templates/ingress-ui.yml.j2 b/roles/tackle/templates/ingress-ui.yml.j2 index 4a26caf7..cd14ae43 100644 --- a/roles/tackle/templates/ingress-ui.yml.j2 +++ b/roles/tackle/templates/ingress-ui.yml.j2 @@ -5,7 +5,7 @@ metadata: annotations: {% if ui_ingress_class_name == 'nginx' %} nginx.ingress.kubernetes.io/proxy-body-size: {{ ui_ingress_proxy_body_size }} -{% if feature_auth_required|bool and feature_auth_type == "keycloak" %} +{% if feature_auth_required|bool %} nginx.ingress.kubernetes.io/force-ssl-redirect: "true" {% endif %} {% elif ui_ingress_class_name == 'alb' %} @@ -27,15 +27,6 @@ spec: rules: - http: paths: -{% if feature_auth_required|bool and feature_auth_type == "keycloak" %} - - path: /auth - pathType: Prefix - backend: - service: - name: {{ keycloak_sso_service_name }} - port: - number: {{ keycloak_sso_port }} -{% endif %} - path: / {% if ui_ingress_path_type is defined %} pathType: {{ ui_ingress_path_type }} diff --git a/roles/tackle/templates/kai/llm-proxy-configmap.yaml.j2 b/roles/tackle/templates/kai/llm-proxy-configmap.yaml.j2 index d6a567fb..f401c598 100644 --- a/roles/tackle/templates/kai/llm-proxy-configmap.yaml.j2 +++ b/roles/tackle/templates/kai/llm-proxy-configmap.yaml.j2 @@ -104,24 +104,11 @@ data: tool_groups: [] server: port: 8321 -{% if feature_auth_required|bool %} - auth: - provider_config: - type: "oauth2_token" - # Skip TLS verification for self-signed certificates (same as hub does) - verify_tls: false - jwks: - # Use the same protocol and base URL that the hub uses for Keycloak -{% if app_profile == 'mta' %} - uri: "{{ rhbk_url }}/auth/realms/{{ keycloak_sso_realm }}/protocol/openid-connect/certs" - # The issuer must match exactly what's in the JWT token from hub auth - issuer: "{{ rhbk_url }}/auth/realms/{{ keycloak_sso_realm }}" -{% else %} - uri: "{{ keycloak_sso_url }}/auth/realms/{{ keycloak_sso_realm }}/protocol/openid-connect/certs" - # The issuer must match exactly what's in the JWT token from hub auth - issuer: "{{ keycloak_sso_url }}/auth/realms/{{ keycloak_sso_realm }}" -{% endif %} - audience: "{{ keycloak_api_audience }}" -{% endif %} + # llm-proxy is reached only through tackle2-hub's /services/llm-proxy + # reverse proxy. The hub validates the bearer token, enforces the + # llm-proxy RBAC scope, and forwards user identity in the X-Hub-User + # header. llm-proxy itself does no token validation — the namespace + # NetworkPolicy is the only barrier against in-namespace callers + # bypassing the hub. Same trust posture as kai-solution-server. telemetry: enabled: false diff --git a/roles/tackle/templates/persistentvolumeclaim-keycloak-postgresql.yml.j2 b/roles/tackle/templates/persistentvolumeclaim-keycloak-postgresql.yml.j2 deleted file mode 100644 index bc09fd01..00000000 --- a/roles/tackle/templates/persistentvolumeclaim-keycloak-postgresql.yml.j2 +++ /dev/null @@ -1,22 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ keycloak_database_data_volume_claim_name }} - namespace: {{ app_namespace }} - labels: - app.kubernetes.io/name: {{ keycloak_database_service_name }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - volume: {{ keycloak_database_data_volume_name }} -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ keycloak_database_data_volume_size }} -{% if rwo_storage_class is defined %} -{% if rwo_storage_class|length %} - storageClassName: {{ rwo_storage_class }} -{% endif %} -{% endif %} diff --git a/roles/tackle/templates/route-ui.yml.j2 b/roles/tackle/templates/route-ui.yml.j2 index 51af3db2..b1044090 100644 --- a/roles/tackle/templates/route-ui.yml.j2 +++ b/roles/tackle/templates/route-ui.yml.j2 @@ -16,9 +16,5 @@ spec: kind: Service name: {{ ui_service_name }} tls: -{% if feature_auth_required|bool and feature_auth_type == "oauth" %} - termination: reencrypt -{% else %} termination: {{ ui_route_tls_termination }} -{% endif %} insecureEdgeTerminationPolicy: {{ ui_route_tls_insecure_termination_policy }} diff --git a/roles/tackle/templates/secret-cookie-secret.yml.j2 b/roles/tackle/templates/secret-cookie-secret.yml.j2 deleted file mode 100644 index bed2d2f5..00000000 --- a/roles/tackle/templates/secret-cookie-secret.yml.j2 +++ /dev/null @@ -1,12 +0,0 @@ -kind: Secret -apiVersion: v1 -metadata: - labels: - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - name: cookie-secret - namespace: {{ app_namespace }} -type: Opaque -data: - cookie-secret: {{ new_cookie_secret | b64encode }} diff --git a/roles/tackle/templates/secret-hub.yml.j2 b/roles/tackle/templates/secret-hub.yml.j2 index 9fcb6d90..684d1e08 100644 --- a/roles/tackle/templates/secret-hub.yml.j2 +++ b/roles/tackle/templates/secret-hub.yml.j2 @@ -11,3 +11,4 @@ type: Opaque data: passphrase: {{ hub_aes_passphrase_b64 }} addon_token: {{ hub_addon_token_b64 }} + apikey_secret: {{ hub_apikey_secret_b64 }} diff --git a/roles/tackle/templates/secret-keycloak-db.yml.j2 b/roles/tackle/templates/secret-keycloak-db.yml.j2 deleted file mode 100644 index ed1fb786..00000000 --- a/roles/tackle/templates/secret-keycloak-db.yml.j2 +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -data: - POSTGRES_DATABASE: {{ rhsso_db_name_b64 }} - POSTGRES_EXTERNAL_ADDRESS: {{ rhsso_db_host_b64 }} - POSTGRES_PASSWORD: {{ rhsso_db_pass_b64 }} - POSTGRES_USERNAME: {{ rhsso_db_user_b64 }} - POSTGRES_SUPERUSER: {{ "true" | b64encode }} - SSLMODE: {{ "prefer" | b64encode }} -kind: Secret -metadata: - labels: - app: keycloak - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - name: keycloak-db-secret - namespace: {{ app_namespace }} -type: Opaque diff --git a/roles/tackle/templates/secret-keycloak-postgresql.yml.j2 b/roles/tackle/templates/secret-keycloak-postgresql.yml.j2 deleted file mode 100644 index f4378264..00000000 --- a/roles/tackle/templates/secret-keycloak-postgresql.yml.j2 +++ /dev/null @@ -1,14 +0,0 @@ -kind: Secret -apiVersion: v1 -metadata: - labels: - app.kubernetes.io/name: {{ keycloak_database_service_name }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - name: {{ keycloak_database_secret_name }} - namespace: {{ app_namespace }} -type: Opaque -data: - database-name: {{ keycloak_database_db_name_b64 }} - database-user: {{ keycloak_database_db_username_b64 }} - database-password: {{ keycloak_database_db_password_b64 }} diff --git a/roles/tackle/templates/secret-keycloak-sso.yml.j2 b/roles/tackle/templates/secret-keycloak-sso.yml.j2 deleted file mode 100644 index 35615d04..00000000 --- a/roles/tackle/templates/secret-keycloak-sso.yml.j2 +++ /dev/null @@ -1,13 +0,0 @@ -kind: Secret -apiVersion: v1 -metadata: - labels: - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - name: {{ keycloak_sso_secret_name }} - namespace: {{ app_namespace }} -type: Opaque -data: - username: {{ keycloak_sso_admin_username_b64 }} - password: {{ keycloak_sso_admin_password_b64 }} diff --git a/roles/tackle/templates/service-keycloak-postgresql-migration.yml.j2 b/roles/tackle/templates/service-keycloak-postgresql-migration.yml.j2 deleted file mode 100644 index c3aa624f..00000000 --- a/roles/tackle/templates/service-keycloak-postgresql-migration.yml.j2 +++ /dev/null @@ -1,21 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/name: {{ keycloak_database_service_name }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - name: {{ keycloak_database_service_k8s_resource_name }}-migration - namespace: {{ app_namespace }} -spec: - ports: - - name: postgres - port: 5432 - targetPort: 5432 - protocol: TCP - selector: - app.kubernetes.io/name: {{ keycloak_database_service_name }}-{{ keycloak_database_db_version }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - version: "{{ keycloak_database_db_version }}" diff --git a/roles/tackle/templates/service-keycloak-postgresql.yml.j2 b/roles/tackle/templates/service-keycloak-postgresql.yml.j2 deleted file mode 100644 index acd0805e..00000000 --- a/roles/tackle/templates/service-keycloak-postgresql.yml.j2 +++ /dev/null @@ -1,21 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/name: {{ keycloak_database_service_name }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - name: {{ keycloak_database_service_k8s_resource_name }} - namespace: {{ app_namespace }} -spec: - ports: - - name: postgres - port: 5432 - targetPort: 5432 - protocol: TCP - selector: - app.kubernetes.io/name: {{ keycloak_database_service_name }}-{{ keycloak_database_db_version }} - app.kubernetes.io/component: {{ keycloak_database_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - version: "{{ keycloak_database_db_version }}" diff --git a/roles/tackle/templates/service-keycloak-rhbk.yml.j2 b/roles/tackle/templates/service-keycloak-rhbk.yml.j2 deleted file mode 100644 index 2f17e98d..00000000 --- a/roles/tackle/templates/service-keycloak-rhbk.yml.j2 +++ /dev/null @@ -1,10 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: -{% if keycloak_sso_tls_enabled|bool and openshift_cluster|bool %} - annotations: - service.beta.openshift.io/serving-cert-secret-name: {{ rhbk_tls_secret_name }} -{% endif %} - name: {{ rhbk_service_name }}-service - namespace: {{ app_namespace }} diff --git a/roles/tackle/templates/service-keycloak-sso.yml.j2 b/roles/tackle/templates/service-keycloak-sso.yml.j2 deleted file mode 100644 index cb9b37d7..00000000 --- a/roles/tackle/templates/service-keycloak-sso.yml.j2 +++ /dev/null @@ -1,29 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: -{% if keycloak_sso_tls_enabled|bool and openshift_cluster|bool %} - annotations: - service.beta.openshift.io/serving-cert-secret-name: {{ keycloak_sso_tls_secret_name }} -{% endif %} - labels: - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - name: {{ keycloak_sso_service_name }} - namespace: {{ app_namespace }} -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - protocol: TCP - - name: https - port: 8443 - targetPort: 8443 - protocol: TCP - selector: - app.kubernetes.io/name: {{ keycloak_sso_service_name }} - app.kubernetes.io/component: {{ keycloak_sso_component_name }} - app.kubernetes.io/part-of: {{ app_name }} - type: ClusterIP diff --git a/roles/tackle/templates/service-ui.yml.j2 b/roles/tackle/templates/service-ui.yml.j2 index 176bd664..8636c72e 100644 --- a/roles/tackle/templates/service-ui.yml.j2 +++ b/roles/tackle/templates/service-ui.yml.j2 @@ -2,7 +2,7 @@ apiVersion: v1 kind: Service metadata: -{% if (ui_tls_enabled|bool and openshift_cluster|bool) or (feature_auth_required|bool and feature_auth_type == "oauth") %} +{% if ui_tls_enabled|bool and openshift_cluster|bool %} annotations: service.beta.openshift.io/serving-cert-secret-name: {{ ui_tls_secret_name }} {% endif %} @@ -14,17 +14,10 @@ metadata: namespace: {{ app_namespace }} spec: ports: -{% if feature_auth_required|bool and feature_auth_type == "oauth" %} - - name: ui - port: {{ oauth_ssl_port }} - targetPort: {{ oauth_ssl_port }} - protocol: TCP -{% else %} - name: ui port: {{ ui_port }} targetPort: {{ ui_port }} protocol: TCP -{% endif %} selector: app.kubernetes.io/name: {{ ui_service_name }} app.kubernetes.io/component: {{ ui_component_name }} diff --git a/roles/tackle/templates/serviceaccount-ui.yml.j2 b/roles/tackle/templates/serviceaccount-ui.yml.j2 index d4df3f2b..fa15645c 100644 --- a/roles/tackle/templates/serviceaccount-ui.yml.j2 +++ b/roles/tackle/templates/serviceaccount-ui.yml.j2 @@ -3,5 +3,3 @@ kind: ServiceAccount metadata: name: {{ ui_serviceaccount_name }} namespace: {{ app_namespace }} - annotations: - serviceaccounts.openshift.io/oauth-redirectreference.primary: '{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"{{ ui_route_name }}"}}' diff --git a/test/e2e/llm-proxy/test-llm-proxy.sh b/test/e2e/llm-proxy/test-llm-proxy.sh index 71f387df..636679dd 100755 --- a/test/e2e/llm-proxy/test-llm-proxy.sh +++ b/test/e2e/llm-proxy/test-llm-proxy.sh @@ -4,9 +4,8 @@ set -e NAMESPACE="${NAMESPACE:-konveyor-tackle}" TEST_FAILED=false -echo "=== Testing LLM Proxy with llemulator backend ===" +echo "=== Testing LLM Proxy via hub /services/llm-proxy ===" -# Wait for services to be ready wait_for_deployment() { local deployment=$1 local retries=0 @@ -14,8 +13,8 @@ wait_for_deployment() { echo -n "Waiting for $deployment..." while [ $retries -lt $max_retries ]; do - READY=$(kubectl get deployment $deployment -n $NAMESPACE -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") - DESIRED=$(kubectl get deployment $deployment -n $NAMESPACE -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") + READY=$(kubectl get deployment "$deployment" -n "$NAMESPACE" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") + DESIRED=$(kubectl get deployment "$deployment" -n "$NAMESPACE" -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") if [ "$READY" == "$DESIRED" ] && [ "$READY" != "0" ]; then echo " ready" @@ -31,75 +30,25 @@ wait_for_deployment() { return 1 } -# Ensure all services are ready wait_for_deployment tackle-hub wait_for_deployment llm-proxy wait_for_deployment llemulator -wait_for_deployment tackle-keycloak-sso -# Wait for Keycloak to be fully ready (not just the pod, but the service responding) -echo -n "Waiting for Keycloak to be ready..." -KC_READY=false -for i in $(seq 1 30); do - KC_STATUS=$(kubectl exec -n $NAMESPACE deployment/tackle-hub -- curl -s -o /dev/null -w "%{http_code}" \ - http://tackle-keycloak-sso:8080/auth/realms/tackle/.well-known/openid-configuration 2>/dev/null || echo "000") - if [ "$KC_STATUS" = "200" ]; then - echo " ready" - KC_READY=true - break - fi - echo -n "." - sleep 5 -done -if [ "$KC_READY" != true ]; then - echo " timeout (Keycloak may not be fully ready)" -fi - -# Wait for admin user to exist in tackle realm (created by keycloak job) -echo -n "Waiting for admin user in tackle realm..." -ADMIN_EXISTS=false -ADMIN_SECRET=$(kubectl get secret tackle-keycloak-sso -n $NAMESPACE -o jsonpath='{.data.password}' 2>/dev/null | base64 -d || true) -for i in $(seq 1 30); do - # Get admin token - ADMIN_TOKEN_RESP=$(kubectl exec -n $NAMESPACE deployment/tackle-hub -- curl -s -X POST \ - http://tackle-keycloak-sso:8080/auth/realms/master/protocol/openid-connect/token \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=password&client_id=admin-cli&username=admin&password=$ADMIN_SECRET" 2>/dev/null || echo "{}") - - if echo "$ADMIN_TOKEN_RESP" | grep -q "access_token"; then - TEMP_TOKEN=$(echo "$ADMIN_TOKEN_RESP" | jq -r '.access_token' 2>/dev/null || true) - if [ -n "$TEMP_TOKEN" ]; then - # Check for admin user in tackle realm - USERS=$(kubectl exec -n $NAMESPACE deployment/tackle-hub -- curl -s \ - "http://tackle-keycloak-sso:8080/auth/admin/realms/tackle/users?username=admin" \ - -H "Authorization: Bearer $TEMP_TOKEN" 2>/dev/null || echo "[]") - if echo "$USERS" | grep -q '"username":"admin"'; then - echo " found" - ADMIN_EXISTS=true - break - fi - fi - fi - echo -n "." - sleep 5 -done -if [ "$ADMIN_EXISTS" != true ]; then - echo " timeout (admin user may not exist yet)" -fi - -# Get hub URL +# Hub is reached via the UI ingress's /hub proxy, which strips the prefix and +# forwards to the hub service. We hit /hub/services/llm-proxy/* so that the +# hub's auth + reverse proxy handle the request; this avoids depending on +# UI-image changes that retarget /llm-proxy at /services/llm-proxy. HUB_URL="" -if kubectl get ingress -n $NAMESPACE &>/dev/null; then - INGRESS_HOST=$(kubectl get ingress tackle -n $NAMESPACE -o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "") +if kubectl get ingress tackle -n "$NAMESPACE" &>/dev/null; then + INGRESS_HOST=$(kubectl get ingress tackle -n "$NAMESPACE" -o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "") if [ -n "$INGRESS_HOST" ]; then HUB_URL="http://$INGRESS_HOST" fi fi -# Fallback to localhost with port-forward if [ -z "$HUB_URL" ]; then - echo "Using port-forward for hub access..." - kubectl port-forward -n $NAMESPACE service/tackle-ui 8080:8080 & + echo "Using port-forward for UI access..." + kubectl port-forward -n "$NAMESPACE" service/tackle-ui 8080:8080 & PF_HUB_PID=$! sleep 3 HUB_URL="http://localhost:8080" @@ -107,197 +56,77 @@ fi echo "Hub URL: $HUB_URL" -# Clear password change requirement for admin user -echo "Configuring authentication..." -# ADMIN_SECRET was already retrieved in the wait loop above - -if [ -z "$ADMIN_SECRET" ]; then - echo " Warning: Could not get Keycloak admin secret, skipping admin user configuration" -else - # Always try to configure admin user (even if wait timed out, user might exist now) - # Get admin token from Keycloak - ADMIN_TOKEN_RESPONSE=$(kubectl exec -n $NAMESPACE deployment/tackle-hub -- curl -s -X POST \ - http://tackle-keycloak-sso:8080/auth/realms/master/protocol/openid-connect/token \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=password" \ - -d "client_id=admin-cli" \ - -d "username=admin" \ - -d "password=$ADMIN_SECRET" 2>/dev/null || echo "{}") - - # Safely extract token (handle non-JSON responses) - if echo "$ADMIN_TOKEN_RESPONSE" | grep -q "access_token"; then - ADMIN_TOKEN=$(echo "$ADMIN_TOKEN_RESPONSE" | jq -r '.access_token // empty' 2>/dev/null || true) - else - ADMIN_TOKEN="" - echo " Warning: Could not get Keycloak admin token (response: $(echo "$ADMIN_TOKEN_RESPONSE" | head -c 200))" - fi - - if [ -n "$ADMIN_TOKEN" ] && [ "$ADMIN_TOKEN" != "null" ]; then - echo " Got Keycloak admin token, configuring admin user..." - # Get admin user ID in tackle realm - USERS_RESPONSE=$(kubectl exec -n $NAMESPACE deployment/tackle-hub -- curl -s \ - http://tackle-keycloak-sso:8080/auth/admin/realms/tackle/users \ - -H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null || echo "[]") - - if echo "$USERS_RESPONSE" | grep -q "username"; then - ADMIN_USER_ID=$(echo "$USERS_RESPONSE" | jq -r '.[] | select(.username=="admin") | .id // empty' 2>/dev/null || true) - else - ADMIN_USER_ID="" - fi - - if [ -n "$ADMIN_USER_ID" ]; then - echo " Found admin user ID: $ADMIN_USER_ID" - # Clear required actions and reset password - kubectl exec -n $NAMESPACE deployment/tackle-hub -- curl -s -X PUT \ - "http://tackle-keycloak-sso:8080/auth/admin/realms/tackle/users/$ADMIN_USER_ID" \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"requiredActions": []}' &>/dev/null || true - - kubectl exec -n $NAMESPACE deployment/tackle-hub -- curl -s -X PUT \ - "http://tackle-keycloak-sso:8080/auth/admin/realms/tackle/users/$ADMIN_USER_ID/reset-password" \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"type": "password", "value": "Passw0rd!", "temporary": false}' &>/dev/null || true - echo " Admin user configured" - else - echo " Warning: Could not find admin user in tackle realm" - fi - else - echo " Warning: Skipping admin user configuration (no admin token)" - fi -fi - -# Test hub authentication (with retries since Keycloak may need time) -echo "Testing hub authentication..." -ACCESS_TOKEN="" -for attempt in 1 2 3; do - echo " Attempt $attempt/3..." - HUB_AUTH_RESPONSE=$(curl -s -X POST \ - $HUB_URL/hub/auth/login \ - -H "Content-Type: application/json" \ - -d '{"user": "admin", "password": "Passw0rd!"}' \ - --connect-timeout 10 \ - --max-time 30 \ - -D /tmp/auth_headers.txt -w "\n%{http_code}" 2>/dev/null || echo "CURL_FAILED") - - # Extract HTTP status code (last line) - HTTP_STATUS=$(echo "$HUB_AUTH_RESPONSE" | tail -1) - # Remove status code from response body - HUB_AUTH_RESPONSE=$(echo "$HUB_AUTH_RESPONSE" | sed '$d') +# Hub PR #1042 seeds a local 'admin' user (login=admin, password=admin) with +# the admin role — see tackle2-hub/internal/auth/seed/users.yaml. Use HTTP +# Basic Auth, which hub parses in internal/auth/request.go. +AUTH_HEADER="Authorization: Basic $(printf 'admin:admin' | base64)" - echo " Hub auth response status: $HTTP_STATUS" +MODEL_ID=$(kubectl get configmap llm-proxy-client -n "$NAMESPACE" -o jsonpath='{.data.config\.json}' 2>/dev/null | jq -r '.model' 2>/dev/null || echo "gpt-4o") - # Check if curl failed or got non-2xx response - if [ "$HTTP_STATUS" = "CURL_FAILED" ]; then - echo " Warning: curl failed to connect to hub" - elif [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 400 ] 2>/dev/null; then - echo " Warning: Hub authentication failed with HTTP $HTTP_STATUS" - echo " Response preview: $(echo "$HUB_AUTH_RESPONSE" | head -c 200)" - else - # Extract token from headers first - ACCESS_TOKEN=$(grep -i "authorization" /tmp/auth_headers.txt 2>/dev/null | sed 's/.*Bearer //' | tr -d '\r\n' || true) - - # If not in headers, try to parse from JSON body - if [ -z "$ACCESS_TOKEN" ]; then - # Only parse if response looks like JSON - if echo "$HUB_AUTH_RESPONSE" | grep -q "^{"; then - ACCESS_TOKEN=$(echo "$HUB_AUTH_RESPONSE" | jq -r '.token // .access_token // empty' 2>/dev/null || true) - fi - fi +EXPECTED_RESPONSES=( + "This is a test response from llemulator for LLM proxy testing." + "The integration between llm-proxy and llemulator is working correctly." + "Test successful: llm-proxy can communicate with the mock OpenAI endpoint." +) - if [ -n "$ACCESS_TOKEN" ] && [ "$ACCESS_TOKEN" != "null" ]; then - echo "Authentication successful" - break - fi - fi - - if [ $attempt -lt 3 ]; then - echo " Retrying in 5 seconds..." - sleep 5 - fi -done - -if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" == "null" ]; then - echo "ERROR: Failed to get authentication token after 3 attempts" - echo " Last response headers: $(cat /tmp/auth_headers.txt 2>/dev/null | head -10)" - TEST_FAILED=true -fi - -# Test LLM proxy with all configured responses echo "Testing LLM proxy endpoint with sequential responses..." -if [ -n "$ACCESS_TOKEN" ]; then - MODEL_ID=$(kubectl get configmap llm-proxy-client -n $NAMESPACE -o jsonpath='{.data.config\.json}' 2>/dev/null | jq -r '.model' 2>/dev/null || echo "gpt-4o") - - # Expected responses in order (from setup-llemulator.sh) - EXPECTED_RESPONSES=( - "This is a test response from llemulator for LLM proxy testing." - "The integration between llm-proxy and llemulator is working correctly." - "Test successful: llm-proxy can communicate with the mock OpenAI endpoint." - ) - - # Test each expected response - for i in "${!EXPECTED_RESPONSES[@]}"; do - echo " Test $((i+1))/3: Verifying response sequence..." +for i in "${!EXPECTED_RESPONSES[@]}"; do + echo " Test $((i+1))/3: Verifying response sequence..." - PROXY_RESPONSE=$(curl -s -X POST ${HUB_URL}/llm-proxy/v1/chat/completions \ - -H "Authorization: Bearer $ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{ - \"model\": \"$MODEL_ID\", - \"messages\": [{\"role\": \"user\", \"content\": \"Test message $((i+1))\"}], - \"max_tokens\": 100 - }" 2>&1) + PROXY_RESPONSE=$(curl -s -X POST "${HUB_URL}/hub/services/llm-proxy/v1/chat/completions" \ + -H "$AUTH_HEADER" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"$MODEL_ID\", + \"messages\": [{\"role\": \"user\", \"content\": \"Test message $((i+1))\"}], + \"max_tokens\": 100 + }" 2>&1) - if echo "$PROXY_RESPONSE" | grep -q "choices"; then - CONTENT=$(echo "$PROXY_RESPONSE" | jq -r '.choices[0].message.content // empty' 2>/dev/null) + if echo "$PROXY_RESPONSE" | grep -q "choices"; then + CONTENT=$(echo "$PROXY_RESPONSE" | jq -r '.choices[0].message.content // empty' 2>/dev/null) - if [ "$CONTENT" = "${EXPECTED_RESPONSES[$i]}" ]; then - echo " ✓ Response $((i+1)) matches expected: correct" - else - echo " ✗ Response $((i+1)) mismatch!" - echo " Expected: ${EXPECTED_RESPONSES[$i]}" - echo " Received: $CONTENT" - TEST_FAILED=true - fi + if [ "$CONTENT" = "${EXPECTED_RESPONSES[$i]}" ]; then + echo " ✓ Response $((i+1)) matches expected" else - echo " ✗ Response $((i+1)) failed - invalid response structure" - echo " Response: $(echo "$PROXY_RESPONSE" | head -1)" + echo " ✗ Response $((i+1)) mismatch!" + echo " Expected: ${EXPECTED_RESPONSES[$i]}" + echo " Received: $CONTENT" TEST_FAILED=true fi - done - - if [ "$TEST_FAILED" != true ]; then - echo "LLM proxy test: PASS - All responses verified" else - echo "LLM proxy test: FAIL - Response verification failed" + echo " ✗ Response $((i+1)) failed - invalid response structure" + echo " Response: $(echo "$PROXY_RESPONSE" | head -1)" + TEST_FAILED=true fi +done + +if [ "$TEST_FAILED" != true ]; then + echo "LLM proxy test: PASS - All responses verified" +else + echo "LLM proxy test: FAIL - Response verification failed" fi -# Test security (invalid token rejection) -echo "Testing security (invalid token rejection)..." -INVALID_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST ${HUB_URL}/llm-proxy/v1/chat/completions \ +echo "Testing security (invalid credentials rejection)..." +INVALID_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${HUB_URL}/hub/services/llm-proxy/v1/chat/completions" \ -H "Authorization: Bearer invalid-token-12345" \ -H "Content-Type: application/json" \ -d "{ - \"model\": \"gpt-4o\", + \"model\": \"$MODEL_ID\", \"messages\": [{\"role\": \"user\", \"content\": \"Test\"}], \"max_tokens\": 5 }") -if [ "$INVALID_STATUS" = "401" ] || [ "$INVALID_STATUS" = "403" ] || [ "$INVALID_STATUS" = "302" ]; then +if [ "$INVALID_STATUS" = "401" ] || [ "$INVALID_STATUS" = "403" ]; then echo "Security test: PASS (HTTP $INVALID_STATUS)" else - echo "Security test: FAIL (HTTP $INVALID_STATUS - expected 401/403/302)" + echo "Security test: FAIL (HTTP $INVALID_STATUS - expected 401/403)" TEST_FAILED=true fi -# Cleanup port-forwards if [ -n "$PF_HUB_PID" ]; then kill $PF_HUB_PID 2>/dev/null || true fi -# Summary echo "" echo "=== Test Summary ===" if [ "$TEST_FAILED" = true ]; then