Skip to content

Commit

Permalink
feat: add STS Accounts API (eclipse-edc#4493)
Browse files Browse the repository at this point in the history
* rename `StsClient` -> `StsAccount`

* add scaffolding for new StsAccountsApi + controller

* add tests for StsAccountController

* Add required methods to the `StsAccountServiceImpl`

* add extension test + auth

* update copyright [skip ci]

* javadoc [skip ci]

* set proper version

* Apply suggestions from code review

Co-authored-by: Enrico Risa <enrico.risa@gmail.com>

* fix javadoc

* DEPENDENCIES

* --amend

---------

Co-authored-by: Enrico Risa <enrico.risa@gmail.com>
  • Loading branch information
paullatzelsperger and wolf4ood authored Sep 26, 2024
1 parent dfa1be6 commit f3b322a
Show file tree
Hide file tree
Showing 54 changed files with 1,695 additions and 419 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-core"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-remote-client"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-api"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-accounts-api"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-client-configuration"))
api(project(":extensions:common:iam:identity-trust:identity-trust-sts:lib:identity-trust-sts-remote-lib"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

plugins {
`java-library`
`maven-publish`
id(libs.plugins.swagger.get().pluginId)
}

dependencies {
api(project(":spi:common:web-spi"))
api(project(":spi:common:auth-spi"))
api(project(":spi:common:identity-trust-sts-spi"))
api(project(":extensions:common:auth:auth-tokenbased"))

implementation(libs.jakarta.rsApi)

testImplementation(libs.jersey.common)
testImplementation(libs.jersey.server)

testImplementation(project(":core:common:junit"))
testImplementation(testFixtures(project(":extensions:common:http:jersey-core")))
testImplementation(testFixtures(project(":spi:common:identity-trust-sts-spi")))
testImplementation(libs.restAssured)
}

edcBuild {
swagger {
apiGroup.set("sts-accounts-api")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.api.iam.identitytrust.sts.accounts;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.eclipse.edc.api.iam.identitytrust.sts.accounts.model.StsAccountCreation;
import org.eclipse.edc.api.iam.identitytrust.sts.accounts.model.UpdateClientSecret;
import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsAccount;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.web.spi.ApiErrorDetail;

import java.util.Collection;

@OpenAPIDefinition
@Tag(name = "Secure Token Service Accounts Api")
public interface StsAccountsApi {

@Operation(description = "Creates a new STS Account with the given parameters",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = StsAccountCreation.class))),
responses = {
@ApiResponse(responseCode = "200", description = "The newly created STS Account including the client_secret.",
content = @Content(schema = @Schema(implementation = StsAccountCreation.class))),
@ApiResponse(responseCode = "400", description = "Invalid Request",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "401", description = "Not authenticated: principal could not be identified",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class))))
})
StsAccountCreation createAccount(StsAccountCreation request);

@Operation(description = "Updates an existing STS account with new values. To update the client secret, please use the relevant API endpoint.",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = StsAccount.class))),
responses = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "400", description = "Invalid Request",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "401", description = "Not authenticated: principal could not be identified",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "404", description = "An account with the given ID was not found")
})
void updateAccount(StsAccount updatedAccount);

@Operation(description = "Gets the STS Account for the given ID",
responses = {
@ApiResponse(responseCode = "200", description = "The STS Account.",
content = @Content(schema = @Schema(implementation = StsAccount.class))),
@ApiResponse(responseCode = "400", description = "Invalid Request",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "401", description = "Not authenticated: principal could not be identified",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "404", description = "An account with the given ID was not found")
})
StsAccount getAccount(String accountId);

@Operation(description = "Queries for STS Account conforming to the given query object",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = QuerySpec.class))),
responses = {
@ApiResponse(responseCode = "200", description = "The STS Accounts.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = StsAccount.class)))),
@ApiResponse(responseCode = "400", description = "Invalid Request",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "401", description = "Not authenticated: principal could not be identified",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class))))
})
Collection<StsAccount> queryAccounts(QuerySpec querySpec);

@Operation(description = "Updates the client secret for an account. If the secret is null, then one will be generated.",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = QuerySpec.class))),
responses = {
@ApiResponse(responseCode = "200", description = "The secret alias that is now used by the account."),
@ApiResponse(responseCode = "400", description = "Invalid Request",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "401", description = "Not authenticated: principal could not be identified",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "404", description = "An account with the given ID was not found")
})
String updateClientSecret(String accountId, UpdateClientSecret request);


@Operation(description = "Deletes an STS Account",
responses = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "400", description = "Invalid Request",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "401", description = "Not authenticated: principal could not be identified",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
@ApiResponse(responseCode = "404", description = "An account with the given ID was not found")
})
void deleteAccount(String accountId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.api.iam.identitytrust.sts.accounts;

import com.fasterxml.jackson.databind.DeserializationFeature;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.SettingContext;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.apiversion.ApiVersionService;
import org.eclipse.edc.spi.system.apiversion.VersionRecord;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.web.spi.WebServer;
import org.eclipse.edc.web.spi.configuration.ApiContext;
import org.eclipse.edc.web.spi.configuration.WebServiceConfigurer;
import org.eclipse.edc.web.spi.configuration.WebServiceSettings;

import java.io.IOException;
import java.util.stream.Stream;

@Extension(value = StsAccountsApiConfigurationExtension.NAME)
public class StsAccountsApiConfigurationExtension implements ServiceExtension {

public static final String NAME = "Secure Token Service Accounts API configuration";
private static final String WEB_SERVICE_NAME = "STS Accounts API";
private static final int DEFAULT_STS_API_PORT = 9393;
private static final String DEFAULT_STS_API_CONTEXT_PATH = "/api/sts";

@SettingContext("Sts API context setting key")
private static final String STS_ACCOUNTS_CONFIG_KEY = "web.http." + ApiContext.STS_ACCOUNTS;

public static final WebServiceSettings SETTINGS = WebServiceSettings.Builder.newInstance()
.apiConfigKey(STS_ACCOUNTS_CONFIG_KEY)
.contextAlias(ApiContext.STS_ACCOUNTS)
.defaultPath(DEFAULT_STS_API_CONTEXT_PATH)
.defaultPort(DEFAULT_STS_API_PORT)
.useDefaultContext(false)
.name(WEB_SERVICE_NAME)
.build();
private static final String API_VERSION_JSON_FILE = "sts-accounts-api-version.json";

@Inject
private WebServer webServer;
@Inject
private WebServiceConfigurer configurator;
@Inject
private TypeManager typeManager;
@Inject
private ApiVersionService apiVersionService;

@Override
public String name() {
return NAME;
}

@Override
public void initialize(ServiceExtensionContext context) {
var config = context.getConfig(STS_ACCOUNTS_CONFIG_KEY);
configurator.configure(config, webServer, SETTINGS);
registerVersionInfo(getClass().getClassLoader());
}

private void registerVersionInfo(ClassLoader resourceClassLoader) {
try (var versionContent = resourceClassLoader.getResourceAsStream(API_VERSION_JSON_FILE)) {
if (versionContent == null) {
throw new EdcException("Version file not found or not readable.");
}
Stream.of(typeManager.getMapper()
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.readValue(versionContent, VersionRecord[].class))
.forEach(vr -> apiVersionService.addRecord(ApiContext.STS_ACCOUNTS, vr));
} catch (IOException e) {
throw new EdcException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.api.iam.identitytrust.sts.accounts;

import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry;
import org.eclipse.edc.api.auth.token.TokenBasedAuthenticationService;
import org.eclipse.edc.api.iam.identitytrust.sts.accounts.controller.StsAccountsApiController;
import org.eclipse.edc.iam.identitytrust.sts.spi.service.StsAccountService;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.web.spi.WebService;
import org.eclipse.edc.web.spi.configuration.ApiContext;

import static java.util.Optional.ofNullable;

@Extension(value = StsAccountsApiExtension.NAME, categories = { "sts", "dcp", "api" })
public class StsAccountsApiExtension implements ServiceExtension {

public static final String NAME = "Secure Token Service Accounts API Extension";
public static final String STS_ACCOUNTS_API_CONTEXT = "sts-accounts-api";

@Setting(value = "API key (or Vault alias) for the STS Accounts API's default authentication mechanism (token-based).")
public static final String STS_ACCOUNTS_API_KEY = "edc.api.accounts.key";

@Inject
private StsAccountService clientService;

@Inject
private WebService webService;
@Inject
private ApiAuthenticationRegistry authenticationRegistry;

@Inject
private Vault vault;

@Override
public String name() {
return NAME;
}

@Override
public void initialize(ServiceExtensionContext context) {

if (!authenticationRegistry.hasService(STS_ACCOUNTS_API_CONTEXT)) {
authenticationRegistry.register(STS_ACCOUNTS_API_CONTEXT, new TokenBasedAuthenticationService(resolveApiKey(context)));
}
var authenticationFilter = new AuthenticationRequestFilter(authenticationRegistry, STS_ACCOUNTS_API_CONTEXT);

webService.registerResource(ApiContext.STS_ACCOUNTS, new StsAccountsApiController(clientService));
webService.registerResource(ApiContext.STS_ACCOUNTS, authenticationFilter);
}

private String resolveApiKey(ServiceExtensionContext context) {
var keyOrAlias = context.getConfig().getString(STS_ACCOUNTS_API_KEY);
return ofNullable(vault.resolveSecret(keyOrAlias))
.orElse(keyOrAlias);
}
}
Loading

0 comments on commit f3b322a

Please sign in to comment.