Skip to content
Draft
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
19 changes: 12 additions & 7 deletions service/tools/maven-plugin/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The plugin uses a goal prefix of `timefold` (see the plugin configuration in the
=== Configure goal (timefold:configure)

- Fetches platform identity/config by calling GET /api/platform/v1/aboutme?includeConfig=true
- Requires a platform personal access token available via environment variable `TIMEFOLD_PAT` (see "Authentication" below)
- Requires a platform personal access token, either via environment variable `TIMEFOLD_PAT` or a local credentials file (see "Authentication" below)
- If a single accountId is returned by the platform and `timefold.accountId` wasn't provided, the plugin will use it
- Writes a properties file at `target/generated-resources/timefold-build.properties` with entries such as:
** `quarkus.container-image.registry` — value taken from platform config.containerRegistry
Expand Down Expand Up @@ -66,17 +66,22 @@ These are the most important configuration properties for the plugin. They are s

- `tfp.model.undeploy.skip` (boolean, default=false) — skip undeploy goal. Note: the property used by the plugin for undeploy skip is `tfp.model.undeploy.skip` (not `timefold.model.undeploy.skip`).

=== Authentication (environment)
=== Authentication

- `TIMEFOLD_PAT` — personal access token for Timefold Platform. The plugin reads this environment variable and sets the `Authorization: Bearer <token>` header on requests.
The token must have the `registered-model:create` and `registered-model:update` scopes; requests will fail with an authorization error if either scope is missing.
See link:https://docs.timefold.ai/timefold-platform/latest/api/platform-api#_authentication_with_personal_access_tokens[Authentication with Personal Access Tokens] for how to create a token.
The plugin resolves the personal access token in this order:

- `TIMEFOLD_PAT` — environment variable. Checked first.
- `~/.timefold/credentials` — a local properties file containing `pat=<your-token>`. Used as a fallback when `TIMEFOLD_PAT` isn't set, so you don't need to export it in every shell session.

Either way, the resolved token is used to set the `Authorization: Bearer <token>` header on requests.
The token must have the `registered-model:create` and `registered-model:update` scopes; requests will fail with an authorization error if either scope is missing.
See link:https://docs.timefold.ai/timefold-platform/latest/api/platform-api#_authentication_with_personal_access_tokens[Authentication with Personal Access Tokens] for how to create a token.

== Headers & HTTP details

Requests include the following headers:

- `Authorization: Bearer <token>` (from TIMEFOLD_PAT)
- `Authorization: Bearer <token>` (from TIMEFOLD_PAT or ~/.timefold/credentials)
- `Content-Type: application/octet-stream` for model upload requests
- `Accept: application/json`
- `X-TF-TENANT-ID` — set to the first tenant if `timefold.model.tenants` is provided
Expand Down Expand Up @@ -148,4 +153,4 @@ Goals are implemented as Mojos in `src/main/java/ai/timefold/solver/tools/maven`
- `UndeployModelMojo` — handles deletion
- `AbstractPlatformModelMojo` — common behavior, HTTP client, descriptor reading

The plugin relies on the environment variable `TIMEFOLD_PAT` for authentication; tests provide a test helper to mock token retrieval.
The plugin relies on `AccessTokenProvider` for authentication, which checks the `TIMEFOLD_PAT` environment variable first, then falls back to `~/.timefold/credentials`; tests provide a test helper to mock token retrieval.
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
package ai.timefold.solver.tools.maven;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class AccessTokenProvider {

private static final Path DEFAULT_CREDENTIALS_FILE = Path.of(System.getProperty("user.home"), ".timefold", "credentials");

private final Path credentialsFile;

public AccessTokenProvider() {
this(DEFAULT_CREDENTIALS_FILE);
}

AccessTokenProvider(Path credentialsFile) {
this.credentialsFile = credentialsFile;
}

public String getAccessToken() {
String envToken = getEnvToken();
if (envToken != null && !envToken.isBlank()) {
return envToken;
}
return readFromCredentialsFile();
}

protected String getEnvToken() {
return System.getenv("TIMEFOLD_PAT");
}

private String readFromCredentialsFile() {
if (!Files.isRegularFile(credentialsFile)) {
return null;
}
Properties properties = new Properties();
try (InputStream input = Files.newInputStream(credentialsFile)) {
properties.load(input);
} catch (IOException e) {
return null;
}
return properties.getProperty("pat");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private PlatformIdentityInfo fetchPlatformConfiguration() {

if (platformPAT == null) {
throw new RuntimeException(
"Personal Access Token for Timefold Platform is required. Set this via TIMEFOLD_PAT environment variable");
"Personal Access Token for Timefold Platform is required. Set this via the TIMEFOLD_PAT environment variable, or store it as 'pat=<token>' in ~/.timefold/credentials");
}

Builder requestBuilder = HttpRequest.newBuilder().GET();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ai.timefold.solver.tools.maven;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class AccessTokenProviderTest {

Check warning on line 12 in service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/AccessTokenProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove redundant visibility modifiers from this test class and its methods.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ_H8mNsIBECVx3O3rBu&open=AZ_H8mNsIBECVx3O3rBu&pullRequest=2559

@TempDir
Path tempDir;

@Test
public void returnsEnvTokenWhenSet() {
AccessTokenProvider provider = new AccessTokenProvider(tempDir.resolve("credentials")) {
@Override
protected String getEnvToken() {
return "env-token";
}
};

assertThat(provider.getAccessToken()).isEqualTo("env-token");
}

@Test
public void fallsBackToCredentialsFileWhenEnvNotSet() throws IOException {
Path credentialsFile = tempDir.resolve("credentials");
Files.writeString(credentialsFile, "pat=file-token\n");

AccessTokenProvider provider = new AccessTokenProvider(credentialsFile) {
@Override
protected String getEnvToken() {
return null;
}
};

assertThat(provider.getAccessToken()).isEqualTo("file-token");
}

@Test
public void returnsNullWhenNeitherEnvNorFileIsSet() {
AccessTokenProvider provider = new AccessTokenProvider(tempDir.resolve("credentials")) {
@Override
protected String getEnvToken() {
return null;
}
};

assertThat(provider.getAccessToken()).isNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public void testConfigureMissingAccessToken(ConfigureMojo mojo) throws Exception

assertThatThrownBy(() -> mojo.execute()).isInstanceOf(RuntimeException.class)
.hasMessage(
"Personal Access Token for Timefold Platform is required. Set this via TIMEFOLD_PAT environment variable");
"Personal Access Token for Timefold Platform is required. Set this via the TIMEFOLD_PAT environment variable, or store it as 'pat=<token>' in ~/.timefold/credentials");

wm1.verify(0, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme")));
}
Expand Down
Loading