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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public class ConnectionPropertyNames {
public static final String OIDC_CONNECTOR_TOKEN_VARIABLE_NAME = "token_variable_name";
public static final String OIDC_DEFAULT_TOKEN_VARIABLE = "jwt.token";

// The teamcity-oidc-plugin's build feature (type "oidc-plugin", display name "OIDC Identity
// Token") that publishes the JWT.
// Its optional "connection_id" param references an oidc-identity-token OAuth connection.
public static final String OIDC_BUILD_FEATURE_TYPE = "oidc-plugin";
public static final String OIDC_BUILD_FEATURE_CONNECTION_ID_PARAM = "connection_id";

public ConnectionPropertyNames() {}

public String getDisplayName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
package octopus.teamcity.server.connection;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

import jetbrains.buildServer.serverSide.ProjectManager;
import jetbrains.buildServer.serverSide.SBuildFeatureDescriptor;
import jetbrains.buildServer.serverSide.SBuildType;
import jetbrains.buildServer.serverSide.SProject;
import jetbrains.buildServer.serverSide.oauth.OAuthConnectionDescriptor;
import jetbrains.buildServer.users.SUser;
import jetbrains.buildServer.web.util.SessionUser;
Expand Down Expand Up @@ -58,15 +61,25 @@ public OctopusConnectionUiData(
*/
@NotNull
public static List<Map<String, String>> availableConnections(final HttpServletRequest request) {
return availableConnections(SessionUser.getUser(request));
return availableConnections(SessionUser.getUser(request), currentBuildType(request));
}

/** As {@link #availableConnections(HttpServletRequest)} but for an already-resolved user. */
static List<Map<String, String>> availableConnections(final SUser user) {
return availableConnections(user, null);
}

static List<Map<String, String>> availableConnections(
final SUser user, final SBuildType buildType) {
final List<Map<String, String>> result = new ArrayList<>();
if (user == null || connectionsManager == null) {
return result;
}

final List<OidcFeatureWarningEvaluator.BuildFeature> oidcFeatures =
buildType == null ? Collections.emptyList() : collectOidcFeatures(buildType);
final SProject project = buildType == null ? null : buildType.getProject();

for (final OAuthConnectionDescriptor descriptor :
connectionsManager.listAvailableConnections(user)) {
final Map<String, String> params = descriptor.getParameters();
Expand All @@ -76,11 +89,59 @@ static List<Map<String, String>> availableConnections(final SUser user) {
view.put("url", params.getOrDefault(CONNECTION_KEYS.getServerUrlPropertyName(), ""));
view.put("version", params.getOrDefault(CONNECTION_KEYS.getVersionPropertyName(), ""));
view.put("space", params.getOrDefault(CONNECTION_KEYS.getSpaceNamePropertyName(), ""));

final String apiKeySource =
params.getOrDefault(CONNECTION_KEYS.getApiKeySourcePropertyName(), "");
final boolean isOidc = ConnectionPropertyNames.API_KEY_SOURCE_OIDC.equals(apiKeySource);
String connectorTokenVariable = "";
OidcFeatureWarningEvaluator.Warning warning = OidcFeatureWarningEvaluator.Warning.NONE;
if (isOidc && buildType != null) {
final String oidcConnectionId =
params.getOrDefault(CONNECTION_KEYS.getOidcConnectionIdPropertyName(), "");
connectorTokenVariable = connectorTokenVariable(project, oidcConnectionId);
warning =
OidcFeatureWarningEvaluator.evaluate(
apiKeySource, oidcConnectionId, connectorTokenVariable, oidcFeatures);
}
view.put("oidcWarning", warning.attributeValue());
view.put(
"oidcExpectedTokenVariable",
isOidc ? OidcFeatureWarningEvaluator.expectedTokenVariable(connectorTokenVariable) : "");
result.add(view);
}
return result;
}

private static List<OidcFeatureWarningEvaluator.BuildFeature> collectOidcFeatures(
final SBuildType buildType) {
final List<OidcFeatureWarningEvaluator.BuildFeature> features = new ArrayList<>();
for (final SBuildFeatureDescriptor descriptor :
buildType.getBuildFeaturesOfType(ConnectionPropertyNames.OIDC_BUILD_FEATURE_TYPE)) {
final Map<String, String> params = descriptor.getParameters();
features.add(
new OidcFeatureWarningEvaluator.BuildFeature(
params.getOrDefault(
ConnectionPropertyNames.OIDC_BUILD_FEATURE_CONNECTION_ID_PARAM, ""),
params.getOrDefault(ConnectionPropertyNames.OIDC_CONNECTOR_TOKEN_VARIABLE_NAME, "")));
}
return features;
}

private static String connectorTokenVariable(
final SProject project, final String oidcConnectionId) {
if (project == null) {
return "";
}
return connectionsManager
.resolve(project, oidcConnectionId)
.map(
connector ->
connector
.getParameters()
.getOrDefault(ConnectionPropertyNames.OIDC_CONNECTOR_TOKEN_VARIABLE_NAME, ""))
.orElse("");
}

/**
* URL of the current project's Connections tab (where connections are added). Built relative to
* the request's context path so the browser resolves it against the host it is actually viewing —
Expand All @@ -95,16 +156,33 @@ public static String editConnectionUrl(final HttpServletRequest request) {
: base + "?projectId=" + projectExternalId + "&tab=oauthConnections";
}

/**
* Context-relative URL of the current build configuration's Build Features tab, where the OIDC
* Identity Token build feature is added. Empty when the build configuration cannot be resolved.
*/
public static String buildFeaturesUrl(final HttpServletRequest request) {
final SBuildType buildType = currentBuildType(request);
if (buildType == null) {
return "";
}
return request.getContextPath()
+ "/admin/editBuildFeatures.html?id=buildType:"
+ buildType.getExternalId();
}

private static String currentProjectExternalId(final HttpServletRequest request) {
final SBuildType buildType = currentBuildType(request);
return buildType == null ? null : buildType.getProject().getExternalId();
}

private static SBuildType currentBuildType(final HttpServletRequest request) {
if (projectManager == null) {
return null;
}
final String idParam = request.getParameter("id");
if (idParam == null || !idParam.startsWith("buildType:")) {
return null;
}
final SBuildType buildType =
projectManager.findBuildTypeByExternalId(idParam.substring("buildType:".length()));
return buildType == null ? null : buildType.getProject().getExternalId();
return projectManager.findBuildTypeByExternalId(idParam.substring("buildType:".length()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright (c) Octopus Deploy and contributors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* these files 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 octopus.teamcity.server.connection;

import java.util.List;

import octopus.teamcity.common.connection.ConnectionPropertyNames;

/**
* Decides which advisory OIDC warning (if any) a step edit form should show for a selected
* connection, from plain data. Kept free of TeamCity types so the branching logic is unit-tested in
* isolation; {@link OctopusConnectionUiData} supplies the inputs.
*
* <p>Supported OIDC setup: the Octopus connection references a connector, and an {@code
* oidc-plugin} build feature references that same connector via {@code connection_id}. An inline
* feature (no {@code connection_id}) is not a supported Octopus OIDC path, so it never satisfies
* the check.
*/
public final class OidcFeatureWarningEvaluator {

/** The warning to display; {@link #attributeValue()} is the DOM data-attribute form. */
public enum Warning {
NONE("none"),
FEATURE_MISSING("feature-missing"),
TOKEN_MISMATCH("token-mismatch");

private final String attributeValue;

Warning(final String attributeValue) {
this.attributeValue = attributeValue;
}

public String attributeValue() {
return attributeValue;
}
}

/** Minimal view of one {@code oidc-plugin} build feature on the build configuration. */
public static final class BuildFeature {
private final String connectionId;
private final String tokenVariableName;

public BuildFeature(final String connectionId, final String tokenVariableName) {
this.connectionId = connectionId == null ? "" : connectionId;
this.tokenVariableName = tokenVariableName == null ? "" : tokenVariableName;
}

public String getConnectionId() {
return connectionId;
}

public String getTokenVariableName() {
return tokenVariableName;
}
}

private OidcFeatureWarningEvaluator() {}

public static Warning evaluate(
final String apiKeySource,
final String oidcConnectionId,
final String connectorTokenVariableName,
final List<BuildFeature> oidcFeatures) {
if (!ConnectionPropertyNames.API_KEY_SOURCE_OIDC.equals(apiKeySource)) {
return Warning.NONE;
}

final String referenced = expectedTokenVariable(connectorTokenVariableName);
boolean anyMatchingFeature = false;
for (final BuildFeature feature : oidcFeatures) {
if (isBlank(feature.getConnectionId())
|| !feature.getConnectionId().trim().equals(oidcConnectionId)) {
continue;
}
anyMatchingFeature = true;
final String published =
isBlank(feature.getTokenVariableName()) ? referenced : feature.getTokenVariableName();
if (published.equals(referenced)) {
return Warning.NONE;
}
}

return anyMatchingFeature ? Warning.TOKEN_MISMATCH : Warning.FEATURE_MISSING;
}

public static String expectedTokenVariable(final String connectorTokenVariableName) {
return isBlank(connectorTokenVariableName)
? ConnectionPropertyNames.OIDC_DEFAULT_TOKEN_VARIABLE
: connectorTokenVariableName;
}

private static boolean isBlank(final String value) {
return value == null || value.trim().isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<%
pageContext.setAttribute("octopusConnections", OctopusConnectionUiData.availableConnections(request));
pageContext.setAttribute("editConnectionUrl", OctopusConnectionUiData.editConnectionUrl(request));
pageContext.setAttribute("buildFeaturesUrl", OctopusConnectionUiData.buildFeaturesUrl(request));
%>

<tr>
Expand All @@ -27,13 +28,34 @@
<span class="octopusConnMeta"
data-conn-id="<c:out value='${conn.id}'/>"
data-conn-space="<c:out value='${conn.space}'/>"
data-conn-version="<c:out value='${conn.version}'/>"></span>
data-conn-version="<c:out value='${conn.version}'/>"
data-conn-oidc-warning="<c:out value='${conn.oidcWarning}'/>"
data-conn-oidc-expected-var="<c:out value='${conn.oidcExpectedTokenVariable}'/>"></span>
</c:forEach>
</span>
<span class="smallNote">
Reuse a connection defined under
<a href="${editConnectionUrl}" target="_blank">Project Settings &raquo; Connections</a>.
</span>
<span class="octopusOidcWarning octopusOidcFeatureMissing error smallNote" style="display:none;">
This connection authenticates using OIDC, but this build configuration has no OIDC Identity
Token build feature referencing its connector. Add one on the
<c:choose>
<c:when test="${not empty buildFeaturesUrl}"><a href="${buildFeaturesUrl}">Build Features</a></c:when>
<c:otherwise>Build Features</c:otherwise>
</c:choose>
page, or the build will fail to authenticate.
</span>
<span class="octopusOidcWarning octopusOidcTokenMismatch error smallNote" style="display:none;">
The OIDC Identity Token build feature for this connector publishes its token under a different
variable name than this connection expects (<code class="octopusExpectedVar"></code>), so the
build will not be able to find the token. Update the
<c:choose>
<c:when test="${not empty buildFeaturesUrl}"><a href="${buildFeaturesUrl}">feature's</a></c:when>
<c:otherwise>feature's</c:otherwise>
</c:choose>
token variable name to <code class="octopusExpectedVar"></code>.
</span>
</td>
</tr>

Expand Down Expand Up @@ -72,11 +94,39 @@
}
}

function updateOctopusOidcWarning() {
const select = document.getElementById("octopusConnectionId");
if (!select) return;
const missingEl = document.querySelector(".octopusOidcFeatureMissing");
const mismatchEl = document.querySelector(".octopusOidcTokenMismatch");
let warning = "none";
let expectedVar = "";
if (select.value !== "") {
const meta = getOctopusConnectionMetadataFor(select.value);
if (meta) {
warning = meta.getAttribute("data-conn-oidc-warning") || "none";
expectedVar = meta.getAttribute("data-conn-oidc-expected-var") || "";
}
}
if (missingEl) {
missingEl.style.display = warning === "feature-missing" ? "" : "none";
}
if (mismatchEl) {
mismatchEl.style.display = warning === "token-mismatch" ? "" : "none";
const varSpans = mismatchEl.querySelectorAll(".octopusExpectedVar");
for (let i = 0; i < varSpans.length; i++) {
varSpans[i].textContent = expectedVar;
}
}
}

$j(document).ready(function () {
const select = document.getElementById("octopusConnectionId");
if (select) {
select.addEventListener("change", toggleOctopusInlineConnectionFields);
select.addEventListener("change", updateOctopusOidcWarning);
toggleOctopusInlineConnectionFields();
updateOctopusOidcWarning();
}
});

Expand Down
Loading
Loading