Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions deploy/mcp-oauth-proxy/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,17 @@ data:
- {{ . | quote }}
{{- end }}
gcp-role-name: {{ .Values.application.tokenExchange.gcpRoleName | default "gcp.fed.mcp.user" | quote }}
{{- $splunk := .Values.application.tokenExchange.splunk | default (dict) }}
splunk:
admin-token-secret-key: {{ $splunk.adminTokenSecretKey | default "splunk-api-stage" | quote }}
mirror-user-prefix: {{ $splunk.mirrorUserPrefix | default "mcp." | quote }}
splunk-token-audience: {{ $splunk.splunkTokenAudience | default "mcp" | quote }}
token-expires-on: {{ $splunk.tokenExpiresOn | default "+1h" | quote }}
{{- $allowedRoles := $splunk.allowedRoles | default (list "yahoo_user" "mcp_user") }}
allowed-roles:
{{- range $allowedRoles }}
- {{ . | quote }}
{{- end }}
oktaServiceTypeApp:
auth-server-url: {{ .Values.application.oktaServiceTypeApp.endpoint | quote }}
client-id: {{ .Values.application.oktaServiceTypeApp.clientId | quote }}
Expand All @@ -319,6 +330,9 @@ data:
- name: embrace
endpoint: {{ .Values.oidc.embrace.authorizationUrl | quote }}
username-claim: {{ .Values.oidc.embrace.claim | quote }}
- name: splunk
endpoint: {{ $splunk.mgmtEndpoint | default "" | quote }}
username-claim: {{ $splunk.usernameClaim | default "short_id" | quote }}
resources:
resource-mapping:
{{- with .Values.application.resourceMapping -}}
Expand Down
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/io/athenz/mop/config/SplunkTokenExchangeConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.athenz.mop.config;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;
import java.util.List;

@ConfigMapping(prefix = "server.token-exchange.splunk")
public interface SplunkTokenExchangeConfig {

/**
* Credentials map key for the Splunk management bearer (must match a key from {@link io.athenz.mop.secret.K8SSecretsProvider#getCredentials}).
* Use {@code splunk-api-stage} or {@code splunk-api-prod} to match Kubernetes secret data keys in {@code mop-credentials}.
*/
@WithName("admin-token-secret-key")
@WithDefault("splunk-api-stage")
String adminTokenSecretKey();

@WithName("mirror-user-prefix")
@WithDefault("mcp.")
String mirrorUserPrefix();

/** Audience parameter passed to Splunk token mint API (not MoP resource audience). */
@WithName("splunk-token-audience")
@WithDefault("mcp")
String splunkTokenAudience();

@WithName("token-expires-on")
@WithDefault("+1h")
String tokenExpiresOn();

/**
* Baseline Splunk roles always assigned to the mirror user (YAML list); defaults when omitted are {@code yahoo_user}
* and {@code mcp_user}. The mirror also receives every role returned for the real Splunk account named by the
* id_token username claim (e.g. {@code short_id}), so the mirror user gets baseline roles plus that account's roles.
*/
@WithName("allowed-roles")
@WithDefault("yahoo_user,mcp_user")
List<String> allowedRoles();
}
18 changes: 18 additions & 0 deletions src/main/java/io/athenz/mop/model/splunk/SplunkTokenEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.athenz.mop.model.splunk;

public record SplunkTokenEntry(SplunkTokenEntryContent content) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.athenz.mop.model.splunk;

/** {@code content} under {@code entry[]} for {@code POST /services/authorization/tokens}. */
public record SplunkTokenEntryContent(String token) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.athenz.mop.model.splunk;

import java.util.List;

/** Subset of Splunk atom feed JSON for authorization token create responses. */
public record SplunkTokensFeedResponse(List<SplunkTokenEntry> entry) {}
19 changes: 19 additions & 0 deletions src/main/java/io/athenz/mop/model/splunk/SplunkUserEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.athenz.mop.model.splunk;

/** One element of the {@code entry} array for user collection responses. */
public record SplunkUserEntry(SplunkUserEntryContent content) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.athenz.mop.model.splunk;

import java.util.List;

/** {@code content} object under {@code entry[]} for {@code /services/authentication/users/...}. */
public record SplunkUserEntryContent(List<String> roles) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright The Athenz Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.athenz.mop.model.splunk;

import java.util.List;

/**
* Subset of Splunk atom feed JSON for {@code GET /services/authentication/users/{name}?output_mode=json}.
*/
public record SplunkUsersFeedResponse(List<SplunkUserEntry> entry) {}
74 changes: 60 additions & 14 deletions src/main/java/io/athenz/mop/secret/K8SSecretsProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jetbrains.annotations.Nullable;
Expand All @@ -37,6 +38,28 @@
@Unremovable
public class K8SSecretsProvider implements CredentialsProvider {

/**
* Kubernetes {@link io.kubernetes.client.openapi.models.V1Secret#getData() Secret.data} keys (and, when the same
* string is used as the {@link #getCredentials} map key, that constant is used for both).
*/
public static final String SECRET_DATA_KEY_CLIENT_SECRET = "client-secret";

public static final String SECRET_DATA_KEY_ATLASSIAN_CLIENT_SECRET = "atlassian-client-secret";
public static final String SECRET_DATA_KEY_GITHUB_CLIENT_SECRET = "github-client-secret";
public static final String SECRET_DATA_KEY_GOOGLE_CLIENT_SECRET = "google-client-secret";
public static final String SECRET_DATA_KEY_EMBRACE_CLIENT_SECRET = "embrace-client-secret";
public static final String SECRET_DATA_KEY_OKTA_TOKEN_EXCHANGE_CLIENT_SECRET = "okta-token-exchange-client-secret";

/** Kubernetes secret data keys for Splunk management API tokens (same names in {@link #getCredentials} map). */
public static final String SECRET_DATA_KEY_SPLUNK_API_STAGE = "splunk-api-stage";
public static final String SECRET_DATA_KEY_SPLUNK_API_PROD = "splunk-api-prod";

/**
* {@link #getCredentials} map key for the value read from {@link #SECRET_DATA_KEY_CLIENT_SECRET} (Okta OIDC
* client secret).
*/
public static final String CREDENTIALS_KEY_OKTA_CLIENT_SECRET = "okta-client-secret";

private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

@ConfigProperty(name = "server.secret.k8s.name")
Expand All @@ -49,27 +72,50 @@ public class K8SSecretsProvider implements CredentialsProvider {
public Map<String, String> getCredentials(String credentialsProviderName) {
try {
Map<String, byte[]> data = getSecretFromApiServer(secretName);
String clientSecret = new String(data.get("client-secret"), StandardCharsets.UTF_8).replaceAll("\\r?\\n$", "");
String atlassianClientSecret = new String(data.get("atlassian-client-secret"), StandardCharsets.UTF_8).replaceAll("\\r?\\n$", "");
String githubClientSecret = new String(data.get("github-client-secret"), StandardCharsets.UTF_8).replaceAll("\\r?\\n$", "");
String googleClientSecret = new String(data.get("google-client-secret"), StandardCharsets.UTF_8).replaceAll("\\r?\\n$", "");
String embraceClientSecret = new String(data.get("embrace-client-secret"), StandardCharsets.UTF_8).replaceAll("\\r?\\n$", "");
String oktaTokenExchangeClientSecret = new String(data.get("okta-token-exchange-client-secret"), StandardCharsets.UTF_8).replaceAll("\\r?\\n$", "");
return Map.of(
"okta-client-secret", clientSecret,
"atlassian-client-secret", atlassianClientSecret,
"github-client-secret", githubClientSecret,
"google-client-secret", googleClientSecret,
"embrace-client-secret", embraceClientSecret,
"okta-token-exchange-client-secret", oktaTokenExchangeClientSecret
);
if (data == null) {
return Map.of();
}
return Map.copyOf(credentialsMapFromData(data));

} catch (IOException | ApiException e) {
log.error("failed to read k8s secret {}", e.getMessage());
}
return Map.of();
}

/**
* Builds the credentials map from raw Kubernetes secret {@code data} entries (test seam for Splunk keys and trimming).
*/
static Map<String, String> credentialsMapFromData(Map<String, byte[]> data) {
String clientSecret = decodeTrimmed(data, SECRET_DATA_KEY_CLIENT_SECRET);
String atlassianClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_ATLASSIAN_CLIENT_SECRET);
String githubClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_GITHUB_CLIENT_SECRET);
String googleClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_GOOGLE_CLIENT_SECRET);
String embraceClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_EMBRACE_CLIENT_SECRET);
String oktaTokenExchangeClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_OKTA_TOKEN_EXCHANGE_CLIENT_SECRET);
String splunkApiStage = decodeTrimmed(data, SECRET_DATA_KEY_SPLUNK_API_STAGE);
String splunkApiProd = decodeTrimmed(data, SECRET_DATA_KEY_SPLUNK_API_PROD);

Map<String, String> map = new HashMap<>();
map.put(CREDENTIALS_KEY_OKTA_CLIENT_SECRET, clientSecret);
map.put(SECRET_DATA_KEY_ATLASSIAN_CLIENT_SECRET, atlassianClientSecret);
map.put(SECRET_DATA_KEY_GITHUB_CLIENT_SECRET, githubClientSecret);
map.put(SECRET_DATA_KEY_GOOGLE_CLIENT_SECRET, googleClientSecret);
map.put(SECRET_DATA_KEY_EMBRACE_CLIENT_SECRET, embraceClientSecret);
map.put(SECRET_DATA_KEY_OKTA_TOKEN_EXCHANGE_CLIENT_SECRET, oktaTokenExchangeClientSecret);
map.put(SECRET_DATA_KEY_SPLUNK_API_STAGE, splunkApiStage);
map.put(SECRET_DATA_KEY_SPLUNK_API_PROD, splunkApiProd);
return map;
}

private static String decodeTrimmed(Map<String, byte[]> data, String key) {
byte[] raw = data.get(key);
if (raw == null) {
return "";
}
return new String(raw, StandardCharsets.UTF_8).replaceAll("\\r?\\n$", "");
}

private @Nullable Map<String, byte[]> getSecretFromApiServer(String pkeySecretName) throws IOException, ApiException {
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/io/athenz/mop/service/AudienceConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.athenz.mop.service;

import org.apache.commons.lang3.StringUtils;

/**
* Audience / provider names used for token storage and token exchange routing.
* Same style as PROVIDER_GLEAN used in resource-mapping token.audience.
Expand All @@ -30,4 +32,16 @@ private AudienceConstants() {
public static final String PROVIDER_GLEAN = "glean";
public static final String PROVIDER_GOOGLE_MONITORING = "google-monitoring";
public static final String PROVIDER_GOOGLE_LOGGING = "google-logging";
public static final String PROVIDER_SPLUNK = "splunk";

/** Exchanged access token stored by audience so {@code GET /userinfo} can resolve Okta profile from Splunk/Glean/GCP tokens. */
public static boolean storesExchangedTokenForUserinfo(String audience) {
if (StringUtils.isBlank(audience)) {
return false;
}
return PROVIDER_GLEAN.equals(audience)
|| PROVIDER_GOOGLE_MONITORING.equals(audience)
|| PROVIDER_GOOGLE_LOGGING.equals(audience)
|| PROVIDER_SPLUNK.equals(audience);
}
}
Loading