diff --git a/it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ConcordTaskIT.java b/it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ConcordTaskIT.java index 5383d5cace..36a356e2d9 100644 --- a/it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ConcordTaskIT.java +++ b/it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ConcordTaskIT.java @@ -64,6 +64,7 @@ public void testCreateProject() throws Exception { */ @Test public void testExternalApiToken() throws Exception { + String username = "user_" + randomString(); UsersApi usersApi = new UsersApi(concord.apiClient()); @@ -217,6 +218,7 @@ public void testDryRunForChildProcess() throws Exception { proc.assertLog(".*Done!.*"); } + @Test public void testCreateApiKey() throws Exception { String apiKeyName1 = "test1_" + randomString(); @@ -259,40 +261,4 @@ public void testCreateApiKey() throws Exception { apiKeys = apiKeysApi.listUserApiKeys(adminId); assertEquals(apiKeyCount, apiKeys.size()); } - - @Test - public void testCreateOrUpdateApiKey() throws Exception { - String username = "user_" + randomString(); - - UsersApi usersApi = new UsersApi(concord.apiClient()); - usersApi.createOrUpdateUser(new CreateUserRequest() - .username(username) - .type(CreateUserRequest.TypeEnum.LOCAL)); - - // -- - - String apiKeyValue = Base64.getEncoder().encodeToString(("foo_" + randomString()).getBytes(UTF_8)); - - Payload payload = new Payload() - .archive(resource("concord/createOrUpdateApiKey")) - .arg("apiKeyValue", apiKeyValue) - .arg("apiKeyUsername", username); - - // --- - - ConcordProcess proc = concord.processes().start(payload); - expectStatus(proc, ProcessEntry.StatusEnum.FINISHED); - - // --- - - proc.assertLog(".*result1=.*"); - proc.assertLog(".*result2=.*"); - proc.assertLog(".*result3=.*"); - - // --- - - ApiKeysApi apiKeysApi = new ApiKeysApi(concord.apiClient().setApiKey(apiKeyValue)); - List apiKeys = apiKeysApi.listUserApiKeys(null); - assertEquals(1, apiKeys.size()); - } } diff --git a/it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/createOrUpdateApiKey/concord.yml b/it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/createOrUpdateApiKey/concord.yml deleted file mode 100644 index de8a4b8534..0000000000 --- a/it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/createOrUpdateApiKey/concord.yml +++ /dev/null @@ -1,31 +0,0 @@ -configuration: - runtime: "concord-v2" - -flows: - default: - # create a new API key - - task: concord - in: - action: createOrUpdateApiKey - username: ${apiKeyUsername} - out: result1 - - log: "result1=${result1}" - - # update the API key - - task: concord - in: - action: createOrUpdateApiKey - username: ${apiKeyUsername} - name: ${result1.name} - out: result2 - - log: "result2=${result2}" - - # update the key again, now using the predefined value - - task: concord - in: - action: createOrUpdateApiKey - username: ${apiKeyUsername} - name: ${result1.name} - key: ${apiKeyValue} - out: result3 - - log: "result3=${result3}" diff --git a/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskCommon.java b/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskCommon.java index fb07ddb3a4..7ab35e40b7 100644 --- a/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskCommon.java +++ b/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskCommon.java @@ -94,8 +94,7 @@ public TaskResult execute(ConcordTaskParams in) throws Exception { kill((KillParams) in); yield TaskResult.success(); } - case CREATEAPIKEY -> createApiKey((CreateOrUpdateApiKeyParams) in); - case CREATEORUPDATEAPIKEY -> createOrUpdateApiKey((CreateOrUpdateApiKeyParams) in); + case CREATEAPIKEY -> createApiKey((CreateApiKeyParams) in); }; } @@ -181,11 +180,23 @@ public void kill(KillParams in) throws Exception { } } - public TaskResult createApiKey(CreateOrUpdateApiKeyParams in) throws Exception { + public TaskResult createApiKey(CreateApiKeyParams in) throws Exception { return withClient(in.baseUrl(), in.apiKey(), client -> { log.info("Creating a new API key in {}", client.getBaseUri()); + UUID userId = in.userId(); + if (userId == null) { + String username = in.username(); + if (username == null) { + throw new IllegalArgumentException("User ID or user name is required"); + } - UUID userId = assertUserId(client, in); + UsersApi usersApi = new UsersApi(client); + UserEntry user = usersApi.findByUsername(username); + if (user == null) { + throw new IllegalArgumentException("User '" + username + "' not found."); + } + userId = user.getId(); + } String keyName = in.name(); if (keyName != null) { @@ -215,51 +226,10 @@ public TaskResult createApiKey(CreateOrUpdateApiKeyParams in) throws Exception { return TaskResult.success() .value("id", response.getId()) - .value("name", response.getName()) .value("key", response.getKey()); }); } - public TaskResult createOrUpdateApiKey(CreateOrUpdateApiKeyParams in) throws Exception { - return withClient(in.baseUrl(), in.apiKey(), client -> { - log.info("Creating or updating an API key in {}", client.getBaseUri()); - - UUID userId = assertUserId(client, in); - - ApiKeysV2Api apiKeysApi = new ApiKeysV2Api(client); - CreateApiKeyResponse response = apiKeysApi.createOrUpdateUserApiKey(new CreateApiKeyRequest() - .name(in.name()) - .userId(userId) - .userDomain(in.userDomain()) - .userType(in.userType()) - .key(in.key())); - - return TaskResult.success() - .value("id", response.getId()) - .value("name", response.getName()) - .value("key", response.getKey()) - .value("result", response.getResult().toString()); - }); - } - - private UUID assertUserId(ApiClient client, CreateOrUpdateApiKeyParams in) throws ApiException { - UUID userId = in.userId(); - if (userId == null) { - String username = in.username(); - if (username == null) { - throw new IllegalArgumentException("User ID or user name is required"); - } - - UsersApi usersApi = new UsersApi(client); - UserEntry user = usersApi.findByUsername(username); - if (user == null) { - throw new IllegalArgumentException("User '" + username + "' not found."); - } - userId = user.getId(); - } - return userId; - } - public Map> getOutVars(String baseUrl, String apiKey, List ids, long timeout) { return waitForCompletion(ids, timeout, p -> { try { diff --git a/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskParams.java b/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskParams.java index 285b2b2e5e..5cb92b1930 100644 --- a/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskParams.java +++ b/plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskParams.java @@ -42,7 +42,7 @@ public static ConcordTaskParams of(Variables input, Map defaults case STARTEXTERNAL -> new StartExternalParams(variables); case FORK -> new ForkParams(variables); case KILL -> new KillParams(variables); - case CREATEAPIKEY, CREATEORUPDATEAPIKEY -> new CreateOrUpdateApiKeyParams(variables); + case CREATEAPIKEY -> new CreateApiKeyParams(variables); }; } @@ -428,7 +428,7 @@ public boolean sync() { } } - public static class CreateOrUpdateApiKeyParams extends ConcordTaskParams { + public static class CreateApiKeyParams extends ConcordTaskParams { private static final String BASE_URL_KEY = "baseUrl"; private static final String API_KEY = "apiKey"; @@ -440,7 +440,7 @@ public static class CreateOrUpdateApiKeyParams extends ConcordTaskParams { private static final String IGNORE_EXISTING_KEY = "ignoreExisting"; private static final String KEY = "key"; - CreateOrUpdateApiKeyParams(Variables variables) { + CreateApiKeyParams(Variables variables) { super(variables); } @@ -535,8 +535,7 @@ public enum Action { STARTEXTERNAL, FORK, KILL, - CREATEAPIKEY, - CREATEORUPDATEAPIKEY, + CREATEAPIKEY } private static class DelegateVariables implements Variables { diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyDao.java b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyDao.java index 78e04e3034..aff5d6b322 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyDao.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyDao.java @@ -32,6 +32,7 @@ import java.util.Base64; import java.util.Base64.Encoder; import java.util.List; +import java.util.Objects; import java.util.UUID; import static com.walmartlabs.concord.server.jooq.tables.ApiKeys.API_KEYS; @@ -90,14 +91,6 @@ public UUID insert(UUID userId, String key, String name, OffsetDateTime expiredA .getKeyId()); } - public void update(UUID keyId, String key, OffsetDateTime expiredAt) { - tx(tx -> tx.update(API_KEYS) - .set(API_KEYS.API_KEY, hash(key)) - .set(API_KEYS.EXPIRED_AT, expiredAt) - .where(API_KEYS.KEY_ID.eq(keyId)) - .execute()); - } - public void delete(UUID id) { tx(tx -> tx.deleteFrom(API_KEYS) .where(API_KEYS.KEY_ID.eq(id)) diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyManager.java b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyManager.java deleted file mode 100644 index d082317ff8..0000000000 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyManager.java +++ /dev/null @@ -1,261 +0,0 @@ -package com.walmartlabs.concord.server.security.apikey; - -/*- - * ***** - * Concord - * ----- - * Copyright (C) 2017 - 2025 Walmart Inc. - * ----- - * 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. - * ===== - */ - -import com.walmartlabs.concord.common.validation.ConcordKey; -import com.walmartlabs.concord.db.PgUtils; -import com.walmartlabs.concord.server.OperationResult; -import com.walmartlabs.concord.server.audit.AuditAction; -import com.walmartlabs.concord.server.audit.AuditLog; -import com.walmartlabs.concord.server.audit.AuditObject; -import com.walmartlabs.concord.server.cfg.ApiKeyConfiguration; -import com.walmartlabs.concord.server.sdk.ConcordApplicationException; -import com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException; -import com.walmartlabs.concord.server.security.Permission; -import com.walmartlabs.concord.server.security.Roles; -import com.walmartlabs.concord.server.security.UnauthorizedException; -import com.walmartlabs.concord.server.security.UserPrincipal; -import com.walmartlabs.concord.server.user.UserManager; -import com.walmartlabs.concord.server.user.UserType; -import org.jooq.exception.DataAccessException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; -import javax.inject.Inject; -import java.time.OffsetDateTime; -import java.util.List; -import java.util.UUID; - -import static java.util.Objects.requireNonNull; - -public class ApiKeyManager { - - private static final Logger log = LoggerFactory.getLogger(ApiKeyManager.class); - - private final ApiKeyDao apiKeyDao; - private final AuditLog auditLog; - private final ApiKeyConfiguration cfg; - private final UserManager userManager; - - @Inject - public ApiKeyManager(ApiKeyConfiguration cfg, - UserManager userManager, - ApiKeyDao apiKeyDao, - AuditLog auditLog) { - - this.cfg = requireNonNull(cfg); - this.userManager = requireNonNull(userManager); - this.apiKeyDao = requireNonNull(apiKeyDao); - this.auditLog = requireNonNull(auditLog); - } - - - public CreateApiKeyResponse create(CreateApiKeyRequest req) { - String key = assertKeyValue(req); - - UUID userId = assertUserId(req.getUserId()); - if (userId == null) { - userId = assertUsername(req.getUsername(), req.getUserDomain(), req.getUserType()); - } - - assertOwner(userId); - - String name = trim(req.getName()); - if (name == null || name.isEmpty()) { - // auto generate the name - name = "key-" + UUID.randomUUID(); - } else { - if (!name.matches(ConcordKey.PATTERN)) { - throw new ValidationErrorsException("Invalid API key name. Must match " + ConcordKey.PATTERN); - } - - if (apiKeyDao.getId(userId, name) != null) { - throw new ValidationErrorsException("API key with name '" + name + "' already exists"); - } - } - - return createApiKey(userId, name, key); - } - - public CreateApiKeyResponse createOrUpdate(CreateApiKeyRequest req) { - String key = assertKeyValue(req); - - UUID userId = assertUserId(req.getUserId()); - if (userId == null) { - userId = assertUsername(req.getUsername(), req.getUserDomain(), req.getUserType()); - } - - if (userId == null) { - userId = UserPrincipal.assertCurrent().getId(); - } - - assertOwner(userId); - - String name = trim(req.getName()); - if (name == null || name.isEmpty()) { - // auto generate the name - name = "key#" + UUID.randomUUID(); - } - - UUID apiKeyId = apiKeyDao.getId(userId, name); - if (apiKeyId == null) { - return createApiKey(userId, name, key); - } else { - return updateApiKey(apiKeyId, userId, name, key); - } - } - - public CreateApiKeyResponse createApiKey(UUID userId, String name, @Nullable String key) { - if (key == null) { - key = apiKeyDao.newApiKey(); - } - - OffsetDateTime expiredAt = null; - if (cfg.isExpirationEnabled()) { - expiredAt = OffsetDateTime.now().plusDays(cfg.getExpirationPeriod().toDays()); - } - - UUID id; - try { - id = apiKeyDao.insert(userId, key, name, expiredAt); - } catch (DataAccessException e) { - if (PgUtils.isUniqueViolationError(e)) { - log.warn("create ['{}'] -> duplicate name error: {}", name, e.getMessage()); - throw new ValidationErrorsException("Duplicate API key name: " + name); - } - - throw e; - } - - auditLog.add(AuditObject.API_KEY, AuditAction.CREATE) - .field("id", id) - .field("name", name) - .field("expiredAt", expiredAt) - .field("userId", userId) - .log(); - - return new CreateApiKeyResponse(id, name, key, OperationResult.CREATED); - } - - public CreateApiKeyResponse updateApiKey(UUID id, UUID userId, String name, @Nullable String key) { - if (key == null) { - key = apiKeyDao.newApiKey(); - } - - OffsetDateTime expiredAt = null; - if (cfg.isExpirationEnabled()) { - expiredAt = OffsetDateTime.now().plusDays(cfg.getExpirationPeriod().toDays()); - } - - apiKeyDao.update(id, key, expiredAt); - - auditLog.add(AuditObject.API_KEY, AuditAction.UPDATE) - .field("id", id) - .field("name", name) - .field("expiredAt", expiredAt) - .field("userId", userId) - .log(); - - return new CreateApiKeyResponse(id, name, key, OperationResult.UPDATED); - } - - public void deleteById(UUID id) { - UUID userId = apiKeyDao.getUserId(id); - if (userId == null) { - throw new ValidationErrorsException("API key not found: " + id); - } - - assertOwner(userId); - - apiKeyDao.delete(id); - - auditLog.add(AuditObject.API_KEY, AuditAction.DELETE) - .field("id", id) - .log(); - } - - public List list(@Nullable UUID userId) { - UUID effectiveUserId = userId; - if (effectiveUserId == null) { - effectiveUserId = UserPrincipal.assertCurrent().getId(); - } - - assertOwner(effectiveUserId); - - return apiKeyDao.list(effectiveUserId); - } - - private UUID assertUsername(String username, String domain, UserType type) { - if (username == null) { - return null; - } - - if (type == null) { - type = UserPrincipal.assertCurrent().getType(); - } - - return userManager.getId(username, domain, type) - .orElseThrow(() -> new ConcordApplicationException("User not found: " + username)); - } - - private UUID assertUserId(UUID userId) { - if (userId == null) { - return null; - } - - if (userManager.get(userId).isEmpty()) { - throw new ValidationErrorsException("User not found: " + userId); - } - - return userId; - } - - private static String assertKeyValue(CreateApiKeyRequest req) { - String key = req.getKey(); - - if (key != null && !Permission.API_KEY_SPECIFY_VALUE.isPermitted()) { - throw new UnauthorizedException("Not allowed to specify the API key value."); - } - - return key; - } - - private static void assertOwner(UUID userId) { - if (Roles.isAdmin()) { - // admin users can manage other user's keys - return; - } - - UserPrincipal p = UserPrincipal.assertCurrent(); - if (!userId.equals(p.getId())) { - throw new UnauthorizedException("Operation is not permitted"); - } - } - - private static String trim(String s) { - if (s == null) { - return null; - } - - return s.trim(); - } -} diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyModule.java b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyModule.java index ce24406a13..f5a4bbb655 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyModule.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyModule.java @@ -38,7 +38,6 @@ public void configure(Binder binder) { newSetBinder(binder, BackgroundTask.class).addBinding().to(ApiKeyLoader.class); bindJaxRsResource(binder, ApiKeyResource.class); - bindJaxRsResource(binder, ApiKeyResourceV2.class); bindSingletonScheduledTask(binder, ApiKeyCleaner.class); bindSingletonScheduledTask(binder, ApiKeyExpirationNotifier.class); diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResource.java b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResource.java index f5a1867110..61ae0fe2cf 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResource.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResource.java @@ -21,44 +21,71 @@ */ import com.walmartlabs.concord.common.validation.ConcordKey; +import com.walmartlabs.concord.db.PgUtils; import com.walmartlabs.concord.server.GenericOperationResult; import com.walmartlabs.concord.server.OperationResult; +import com.walmartlabs.concord.server.audit.AuditAction; +import com.walmartlabs.concord.server.audit.AuditLog; +import com.walmartlabs.concord.server.audit.AuditObject; +import com.walmartlabs.concord.server.cfg.ApiKeyConfiguration; +import com.walmartlabs.concord.server.sdk.ConcordApplicationException; import com.walmartlabs.concord.server.sdk.rest.Resource; import com.walmartlabs.concord.server.sdk.validation.Validate; import com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException; +import com.walmartlabs.concord.server.security.Permission; import com.walmartlabs.concord.server.security.Roles; +import com.walmartlabs.concord.server.security.UnauthorizedException; +import com.walmartlabs.concord.server.security.UserPrincipal; +import com.walmartlabs.concord.server.user.UserManager; +import com.walmartlabs.concord.server.user.UserType; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.shiro.authz.AuthorizationException; +import org.jooq.exception.DataAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.validation.Valid; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; -import static java.util.Objects.requireNonNull; - @Path("/api/v1/apikey") @Tag(name = "API keys") public class ApiKeyResource implements Resource { + private static final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); + + private final ApiKeyConfiguration cfg; private final ApiKeyDao apiKeyDao; - private final ApiKeyManager apiKeyManager; + private final UserManager userManager; + private final AuditLog auditLog; @Inject - public ApiKeyResource(ApiKeyDao apiKeyDao, ApiKeyManager apiKeyManager) { - this.apiKeyDao = requireNonNull(apiKeyDao); - this.apiKeyManager = requireNonNull(apiKeyManager); + public ApiKeyResource(ApiKeyConfiguration cfg, ApiKeyDao apiKeyDao, UserManager userManager, AuditLog auditLog) { + this.cfg = cfg; + this.apiKeyDao = apiKeyDao; + this.userManager = userManager; + this.auditLog = auditLog; } @GET @Produces(MediaType.APPLICATION_JSON) @Validate @Operation(description = "List user api keys", operationId = "listUserApiKeys") - public List list(@QueryParam("userId") UUID userId) { - return apiKeyManager.list(userId); + public List list(@QueryParam("userId") UUID requestUserId) { + UUID userId = requestUserId; + if (userId == null) { + userId = UserPrincipal.assertCurrent().getId(); + } + + assertOwner(userId); + + return apiKeyDao.list(userId); } @POST @@ -74,7 +101,7 @@ public CreateApiKeyResponse create(@PathParam("name") @ConcordKey String name) { throw new ValidationErrorsException("API Token with name '" + name + "' already exists"); } - return apiKeyManager.createApiKey(null, name, null); + return createApiKey(null, name, null); } @POST @@ -83,7 +110,34 @@ public CreateApiKeyResponse create(@PathParam("name") @ConcordKey String name) { @Validate @Operation(description = "Create a new API key", operationId = "createUserApiKey") public CreateApiKeyResponse create(@Valid CreateApiKeyRequest req) { - return apiKeyManager.create(req); + String key = req.getKey(); + + if (key != null && !Permission.API_KEY_SPECIFY_VALUE.isPermitted()) { + throw new UnauthorizedException("Not allowed to specify the API key value."); + } + + UUID userId = assertUserId(req.getUserId()); + if (userId == null) { + userId = assertUsername(req.getUsername(), req.getUserDomain(), req.getUserType()); + } + + if (userId == null) { + userId = UserPrincipal.assertCurrent().getId(); + } + + assertOwner(userId); + + String name = trim(req.getName()); + if (name == null || name.isEmpty()) { + // auto generate the name + name = "key#" + UUID.randomUUID(); + } + + if (apiKeyDao.getId(userId, name) != null) { + throw new ValidationErrorsException("API Token with name '" + name + "' already exists"); + } + + return createApiKey(userId, name, key); } @DELETE @@ -92,13 +146,102 @@ public CreateApiKeyResponse create(@Valid CreateApiKeyRequest req) { @Validate @Operation(description = "Delete an existing API key", operationId = "deleteUserApiKeyById") public GenericOperationResult deleteKeyById(@PathParam("id") UUID id) { - apiKeyManager.deleteById(id); + UUID userId = apiKeyDao.getUserId(id); + if (userId == null) { + throw new ValidationErrorsException("API key not found: " + id); + } + + assertOwner(userId); + + apiKeyDao.delete(id); + + auditLog.add(AuditObject.API_KEY, AuditAction.DELETE) + .field("id", id) + .log(); + return new GenericOperationResult(OperationResult.DELETED); } + private CreateApiKeyResponse createApiKey(UUID userId, String name, @Nullable String key) { + if (key == null) { + key = apiKeyDao.newApiKey(); + } + + OffsetDateTime expiredAt = null; + if (cfg.isExpirationEnabled()) { + expiredAt = OffsetDateTime.now().plusDays(cfg.getExpirationPeriod().toDays()); + } + + UUID id; + try { + id = apiKeyDao.insert(userId, key, name, expiredAt); + } catch (DataAccessException e) { + if (PgUtils.isUniqueViolationError(e)) { + log.warn("create ['{}'] -> duplicate name error: {}", name, e.getMessage()); + throw new ValidationErrorsException("Duplicate API key name: " + name); + } + + throw e; + } + + auditLog.add(AuditObject.API_KEY, AuditAction.CREATE) + .field("id", id) + .field("name", name) + .field("expiredAt", expiredAt) + .field("userId", userId) + .log(); + + return new CreateApiKeyResponse(id, key); + } + + private UUID assertUsername(String username, String domain, UserType type) { + if (username == null) { + return null; + } + + if (type == null) { + type = UserPrincipal.assertCurrent().getType(); + } + + return userManager.getId(username, domain, type) + .orElseThrow(() -> new ConcordApplicationException("User not found: " + username)); + } + + private UUID assertUserId(UUID userId) { + if (userId == null) { + return null; + } + + if (!userManager.get(userId).isPresent()) { + throw new ValidationErrorsException("User not found: " + userId); + } + + return userId; + } + + private static void assertOwner(UUID userId) { + if (Roles.isAdmin()) { + // admin users can manage other user's keys + return; + } + + UserPrincipal p = UserPrincipal.assertCurrent(); + if (!userId.equals(p.getId())) { + throw new UnauthorizedException("Operation is not permitted"); + } + } + private static void assertAdmin() { if (!Roles.isAdmin()) { - throw new AuthorizationException("Only admins are allowed to create API keys without users"); + throw new AuthorizationException("Only admins are allowed to update organizations"); + } + } + + private static String trim(String s) { + if (s == null) { + return null; } + + return s.trim(); } } diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResourceV2.java b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResourceV2.java deleted file mode 100644 index 90f8a3297f..0000000000 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResourceV2.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.walmartlabs.concord.server.security.apikey; - -/*- - * ***** - * Concord - * ----- - * Copyright (C) 2017 - 2025 Walmart Inc. - * ----- - * 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. - * ===== - */ - -import com.walmartlabs.concord.server.sdk.rest.Resource; -import com.walmartlabs.concord.server.sdk.validation.Validate; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; - -import javax.inject.Inject; -import javax.validation.Valid; -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -import static java.util.Objects.requireNonNull; - -@Path("/api/v2/apikey") -@Tag(name = "API keys V2") -public class ApiKeyResourceV2 implements Resource { - - private final ApiKeyManager apiKeyManager; - - @Inject - public ApiKeyResourceV2(ApiKeyManager apiKeyManager) { - this.apiKeyManager = requireNonNull(apiKeyManager); - } - - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Validate - @Operation(description = "Create a new API key or update an existing one", operationId = "createOrUpdateUserApiKey") - public CreateApiKeyResponse createOrUpdate(@Valid CreateApiKeyRequest req) { - return apiKeyManager.createOrUpdate(req); - } -} diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyRequest.java b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyRequest.java index 13b6abd60f..8e27dcc754 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyRequest.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyRequest.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.walmartlabs.concord.common.validation.ConcordKey; import com.walmartlabs.concord.server.user.UserType; import java.io.Serial; @@ -37,6 +38,7 @@ public class CreateApiKeyRequest implements Serializable { private final String username; private final String userDomain; private final UserType userType; + @ConcordKey private final String name; private final String key; diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyResponse.java b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyResponse.java index 18a6db2be2..7ff5856e51 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyResponse.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyResponse.java @@ -22,42 +22,28 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.walmartlabs.concord.server.OperationResult; -import java.io.Serial; import java.io.Serializable; import java.util.UUID; public class CreateApiKeyResponse implements Serializable { - @Serial private static final long serialVersionUID = 1L; private final boolean ok = true; private final UUID id; - private final String name; private final String key; - private final OperationResult result; @JsonCreator - public CreateApiKeyResponse(@JsonProperty("id") UUID id, - @JsonProperty("name") String name, - @JsonProperty("key") String key, - @JsonProperty("result") OperationResult result) { + public CreateApiKeyResponse(@JsonProperty("id") UUID id, @JsonProperty("key") String key) { this.id = id; - this.name = name; this.key = key; - this.result = result; } public UUID getId() { return id; } - public String getName() { - return name; - } - public String getKey() { return key; } @@ -66,17 +52,11 @@ public boolean isOk() { return ok; } - public OperationResult getResult() { - return result; - } - @Override public String toString() { return "CreateApiKeyResponse{" + "ok=" + ok + - ", result='" + result + '\'' + ", id=" + id + - ", name=" + name + ", key='" + key + '\'' + '}'; }