Skip to content

Commit

Permalink
initial working solution with sync-fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwinslowtech committed Oct 31, 2024
1 parent 7c42d41 commit b6ed484
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 109 deletions.
45 changes: 45 additions & 0 deletions buildScripts/publish-extension.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -eu
rm -f *.vsix

PUBLISHER="winslowtech"
ORGANIZATION="winslowtech"
EXTENSION_ID="novo-artifactory-setup"
EXTENSION_NAME="Novo Artifactory Setup"

OVERRIDE_JSON=$(
cat <<EOF
{
"id": "$EXTENSION_ID",
"name": "$EXTENSION_NAME",
"public": false,
"description": "work in progress"
}
EOF
)

# Unshare and unpublish the extension if it exists (ignore errors if it doesn't)
tfx extension unshare -t "$WINSLOWTECH_TOKEN" --extension-id $EXTENSION_ID --publisher "$PUBLISHER" --unshare-with "$ORGANIZATION" 2>/dev/null || true
tfx extension unpublish -t "$WINSLOWTECH_TOKEN" --extension-id $EXTENSION_ID --publisher "$PUBLISHER" || true

# Create the VSIX package
tfx extension create --manifests vss-extension.json --publisher "$PUBLISHER" --rev-version --override $OVERRIDE_JSON

vsixSize=$(du -k *.vsix | awk '{sum+=$1} END {print int(sum/1024)}')
echo "Extension VSIX size is ${vsixSize}MB"

# Publish the extension
tfx extension publish -t "$WINSLOWTECH_TOKEN" \
--publisher "$PUBLISHER" \
--manifests vss-extension.json \
--share-with "$ORGANIZATION" \
--override $OVERRIDE_JSON

# Install the extension to your organization
tfx extension install \
--publisher "$PUBLISHER" \
--extension-id $EXTENSION_ID \
--service-url "https://${PUBLISHER}.visualstudio.com" \
-t "$WINSLOWTECH_TOKEN"

rm *.vsix
3 changes: 2 additions & 1 deletion jfrog-tasks-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
"typings": "utils.d.ts",
"dependencies": {
"azure-pipelines-task-lib": "4.5.0",
"azure-pipelines-tool-lib": "2.0.6",
"azure-pipelines-tasks-java-common": "^2.219.1",
"azure-pipelines-tool-lib": "2.0.6",
"sync-fetch": "^0.5.2",
"typed-rest-client": "^1.8.11"
},
"scripts": {
Expand Down
85 changes: 84 additions & 1 deletion jfrog-tasks-utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const execSync = require('child_process').execSync;
const toolLib = require('azure-pipelines-tool-lib/tool');
const credentialsHandler = require('typed-rest-client/Handlers');
const findJavaHome = require('azure-pipelines-tasks-java-common/java-common').findJavaHome;

const fileName = getCliExecutableName();
const jfrogCliToolName = 'jf';
const cliPackage = 'jfrog-cli-' + getArchitecture();
Expand All @@ -19,6 +18,7 @@ const buildAgent = 'jfrog-azure-devops-extension';
const customFolderPath = encodePath(join(jfrogFolderPath, 'current'));
const customCliPath = encodePath(join(customFolderPath, fileName)); // Optional - Customized jfrog-cli path.
const jfrogCliReleasesUrl = 'https://releases.jfrog.io/artifactory/jfrog-cli/v2-jf';
const syncFetch = require('sync-fetch');

// Set by Tools Installer Task. This JFrog CLI version will be used in all tasks unless manual installation is used,
// or a specific version was requested in a task. If not set, use the default CLI version.
Expand Down Expand Up @@ -160,6 +160,13 @@ function createAuthHandlers(serviceConnection) {
let artifactoryUser = tl.getEndpointAuthorizationParameter(serviceConnection, 'username', true);
let artifactoryPassword = tl.getEndpointAuthorizationParameter(serviceConnection, 'password', true);
let artifactoryAccessToken = tl.getEndpointAuthorizationParameter(serviceConnection, 'apitoken', true);
let oidcProviderName = tl.getEndpointAuthorizationParameter(service, 'oidcProviderName', true);
let jfrogPlatformUrl = tl.getEndpointAuthorizationParameter(service, 'jfrogPlatformUrl', true);

if (oidcProviderName) {
const adoJWT = getADOJWT(serviceConnection);
serviceAccessToken = getArtifactoryAccessToken(adoJWT, oidcProviderName, jfrogPlatformUrl);
}

// Check if Artifactory should be accessed using access-token.
if (artifactoryAccessToken) {
Expand Down Expand Up @@ -252,15 +259,91 @@ function configureDistributionCliServer(distributionService, serverId, cliPath,
function configureXrayCliServer(xrayService, serverId, cliPath, buildDir) {
return configureSpecificCliServer(xrayService, '--xray-url', serverId, cliPath, buildDir);
}
function logIDToken(oidcToken) {
const oidcClaims = JSON.parse(Buffer.from(oidcToken.split('.')[1], 'base64').toString());
console.log('OIDC Token Subject: ', oidcClaims.sub);
console.log(`OIDC Token Claims: {"sub": "${oidcClaims.sub}"}`);
console.log('OIDC Token Issuer (Provider URL): ', oidcClaims.iss);
console.log('OIDC Token Audience: ', oidcClaims.aud);
}

function getADOJWT(serviceConnectionID) {
const uri = getValue('System.CollectionUri');
const teamPrjID = getValue('System.TeamProjectId');
const hub = getValue('System.HostType');
const planID = getValue('System.PlanId');
const jobID = getValue('System.JobId');

const url = `${uri}${teamPrjID}/_apis/distributedtask/hubs/${hub}/plans/${planID}/jobs/${jobID}/oidctoken?api-version=7.1-preview.1&serviceConnectionId=${serviceConnectionID}`;

try {
const response = syncFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getValue('System.AccessToken')}`,
},
});

jfrogAccessToken = response.json().oidcToken;
logIDToken(jfrogAccessToken);
return jfrogAccessToken;
} catch (error) {
throw new Error(`Failed to get or parse response: ${error.message}`);
}
}

function getArtifactoryAccessToken(adoJWT, oidcProviderName, jfrogPlatformUrl) {
const payload = {
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
subject_token: adoJWT,
provider_name: oidcProviderName,
};

const url = `${jfrogPlatformUrl}/access/api/v1/oidc/token`;

try {
const res = syncFetch(url, {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
});

if (!res.ok) {
throw new Error(`Failed to get the Artifactory access token: ${res.statusText}`);
}
return res.json().access_token;
} catch (err) {
throw new Error(`Failed to get or parse response: ${err.message}`);
}
}

function getValue(key) {
const variable = tl.getVariable(key);
if (!variable) {
throw new Error(`Required variable '${key}' returned undefined!`);
}

return variable;
}

function configureSpecificCliServer(service, urlFlag, serverId, cliPath, buildDir) {
let serviceUrl = tl.getEndpointUrl(service, false);
let serviceUser = tl.getEndpointAuthorizationParameter(service, 'username', true);
let servicePassword = tl.getEndpointAuthorizationParameter(service, 'password', true);
let serviceAccessToken = tl.getEndpointAuthorizationParameter(service, 'apitoken', true);
let oidcProviderName = tl.getEndpointAuthorizationParameter(service, 'oidcProviderName', true);
let jfrogPlatformUrl = tl.getEndpointAuthorizationParameter(service, 'jfrogPlatformUrl', true);
let cliCommand = cliJoin(cliPath, jfrogCliConfigAddCommand, quote(serverId), urlFlag + '=' + quote(serviceUrl), '--interactive=false');
let stdinSecret;
let secretInStdinSupported = isStdinSecretSupported();

if (oidcProviderName) {
const adoJWT = getADOJWT(service);
serviceAccessToken = getArtifactoryAccessToken(adoJWT, oidcProviderName, jfrogPlatformUrl);
}

if (serviceAccessToken) {
// Add access-token if required.
cliCommand = cliJoin(cliCommand, secretInStdinSupported ? '--access-token-stdin' : '--access-token=' + quote(serviceAccessToken));
Expand Down
Loading

0 comments on commit b6ed484

Please sign in to comment.