From a121af7688a2e8cd247005fd0a7200f4aa9a3bd1 Mon Sep 17 00:00:00 2001 From: Lin Date: Mon, 9 Dec 2024 10:58:49 -0800 Subject: [PATCH] feat: implement public create principal and related tests (#1956) --- .../measurement/access/service/BUILD.bazel | 1 + .../measurement/access/service/Errors.kt | 24 +- .../access/service/v1alpha/BUILD.bazel | 1 + .../service/v1alpha/PrincipalsService.kt | 72 +++++ .../service/v1alpha/ResourceConversion.kt | 31 ++- .../access/v1alpha/principals_service.proto | 3 + .../service/v1alpha/PrincipalsServiceTest.kt | 248 ++++++++++++++++++ 7 files changed, 375 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/wfanet/measurement/access/service/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/access/service/BUILD.bazel index a189e46499e..4ac4c950f83 100644 --- a/src/main/kotlin/org/wfanet/measurement/access/service/BUILD.bazel +++ b/src/main/kotlin/org/wfanet/measurement/access/service/BUILD.bazel @@ -25,6 +25,7 @@ kt_jvm_library( "//src/main/kotlin/org/wfanet/measurement/access/service/internal:errors", "//src/main/kotlin/org/wfanet/measurement/common/grpc:error_info", "//src/main/proto/google/rpc:error_details_kt_jvm_proto", + "//src/main/proto/wfa/measurement/access/v1alpha:principal_kt_jvm_proto", "@wfa_common_jvm//imports/java/io/grpc:api", ], ) diff --git a/src/main/kotlin/org/wfanet/measurement/access/service/Errors.kt b/src/main/kotlin/org/wfanet/measurement/access/service/Errors.kt index 62a632d8c1f..bceaf6779b7 100644 --- a/src/main/kotlin/org/wfanet/measurement/access/service/Errors.kt +++ b/src/main/kotlin/org/wfanet/measurement/access/service/Errors.kt @@ -21,6 +21,7 @@ import io.grpc.Status import io.grpc.StatusException import io.grpc.StatusRuntimeException import org.wfanet.measurement.access.service.internal.Errors as InternalErrors +import org.wfanet.measurement.access.v1alpha.Principal import org.wfanet.measurement.common.grpc.Errors as CommonErrors import org.wfanet.measurement.common.grpc.errorInfo @@ -45,7 +46,7 @@ object Errors { RESOURCE_TYPE_NOT_FOUND_IN_PERMISSION, REQUIRED_FIELD_NOT_SET, INVALID_FIELD_VALUE, - ETAG_MISMATCH + ETAG_MISMATCH, } enum class Metadata(val key: String) { @@ -61,7 +62,7 @@ object Errors { ISSUER("issuer"), SUBJECT("subject"), REQUEST_ETAG("requestEtag"), - ETAG("etag") + ETAG("etag"), } } @@ -153,6 +154,25 @@ class PrincipalNotFoundException(name: String, cause: Throwable? = null) : cause, ) +class PrincipalAlreadyExistsException(cause: Throwable? = null) : + ServiceException( + Errors.Reason.PRINCIPAL_ALREADY_EXISTS, + "Principal already exists", + emptyMap(), + cause, + ) + +class PrincipalTypeNotSupportedException( + identityCase: Principal.IdentityCase, + cause: Throwable? = null, +) : + ServiceException( + Errors.Reason.PRINCIPAL_TYPE_NOT_SUPPORTED, + "Principal type ${identityCase.name} not supported", + mapOf(Errors.Metadata.PRINCIPAL_TYPE to identityCase.name), + cause, + ) + class PermissionNotFoundException(name: String, cause: Throwable? = null) : ServiceException( reason, diff --git a/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/BUILD.bazel index 29b76c688e1..a8460857e93 100644 --- a/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/BUILD.bazel +++ b/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/BUILD.bazel @@ -15,6 +15,7 @@ kt_jvm_library( ":resource_conversion", "//src/main/kotlin/org/wfanet/measurement/access/service:errors", "//src/main/kotlin/org/wfanet/measurement/access/service:resource_key", + "//src/main/kotlin/org/wfanet/measurement/common/api:resource_ids", "//src/main/proto/wfa/measurement/access/v1alpha:principals_service_kt_jvm_grpc_proto", "//src/main/proto/wfa/measurement/internal/access:principals_service_kt_jvm_grpc_proto", ], diff --git a/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsService.kt b/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsService.kt index 85e3320e3bb..979450ef76c 100644 --- a/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsService.kt +++ b/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsService.kt @@ -19,15 +19,20 @@ package org.wfanet.measurement.access.service.v1alpha import io.grpc.Status import io.grpc.StatusException import org.wfanet.measurement.access.service.InvalidFieldValueException +import org.wfanet.measurement.access.service.PrincipalAlreadyExistsException import org.wfanet.measurement.access.service.PrincipalKey import org.wfanet.measurement.access.service.PrincipalNotFoundException +import org.wfanet.measurement.access.service.PrincipalTypeNotSupportedException import org.wfanet.measurement.access.service.RequiredFieldNotSetException import org.wfanet.measurement.access.service.internal.Errors as InternalErrors +import org.wfanet.measurement.access.v1alpha.CreatePrincipalRequest import org.wfanet.measurement.access.v1alpha.GetPrincipalRequest import org.wfanet.measurement.access.v1alpha.Principal import org.wfanet.measurement.access.v1alpha.PrincipalsGrpcKt +import org.wfanet.measurement.common.api.ResourceIds import org.wfanet.measurement.internal.access.Principal as InternalPrincipal import org.wfanet.measurement.internal.access.PrincipalsGrpcKt.PrincipalsCoroutineStub as InternalPrincipalsCoroutineStub +import org.wfanet.measurement.internal.access.createUserPrincipalRequest as internalCreateUserPrincipalRequest import org.wfanet.measurement.internal.access.getPrincipalRequest as internalGetPrincipalRequest class PrincipalsService(private val internalPrincipalsStub: InternalPrincipalsCoroutineStub) : @@ -75,4 +80,71 @@ class PrincipalsService(private val internalPrincipalsStub: InternalPrincipalsCo return internalResponse.toPrincipal() } + + override suspend fun createPrincipal(request: CreatePrincipalRequest): Principal { + when (request.principal.identityCase) { + Principal.IdentityCase.USER -> {} + Principal.IdentityCase.TLS_CLIENT -> + throw PrincipalTypeNotSupportedException(request.principal.identityCase) + .asStatusRuntimeException(Status.Code.INVALID_ARGUMENT) + else -> + throw RequiredFieldNotSetException("principal.identity") + .asStatusRuntimeException(Status.Code.INVALID_ARGUMENT) + } + + if (request.principal.user.issuer.isEmpty()) { + throw RequiredFieldNotSetException("principal.user.issuer") + .asStatusRuntimeException(Status.Code.INVALID_ARGUMENT) + } + + if (request.principal.user.subject.isEmpty()) { + throw RequiredFieldNotSetException("principal.user.subject") + .asStatusRuntimeException(Status.Code.INVALID_ARGUMENT) + } + + if (request.principalId.isEmpty()) { + throw RequiredFieldNotSetException("principal_id") + .asStatusRuntimeException(Status.Code.INVALID_ARGUMENT) + } + + if (!ResourceIds.RFC_1034_REGEX.matches(request.principalId)) { + throw InvalidFieldValueException("principal_id") + .asStatusRuntimeException(Status.Code.INVALID_ARGUMENT) + } + + val internalResponse: InternalPrincipal = + try { + internalPrincipalsStub.createUserPrincipal( + internalCreateUserPrincipalRequest { + principalResourceId = request.principalId + user = request.principal.user.toInternal() + } + ) + } catch (e: StatusException) { + throw when (InternalErrors.getReason(e)) { + InternalErrors.Reason.PRINCIPAL_ALREADY_EXISTS -> + PrincipalAlreadyExistsException(e).asStatusRuntimeException(Status.Code.ALREADY_EXISTS) + InternalErrors.Reason.PRINCIPAL_NOT_FOUND, + InternalErrors.Reason.PRINCIPAL_NOT_FOUND_FOR_USER, + InternalErrors.Reason.PRINCIPAL_NOT_FOUND_FOR_TLS_CLIENT, + InternalErrors.Reason.PRINCIPAL_TYPE_NOT_SUPPORTED, + InternalErrors.Reason.PERMISSION_NOT_FOUND, + InternalErrors.Reason.PERMISSION_NOT_FOUND_FOR_ROLE, + InternalErrors.Reason.ROLE_NOT_FOUND, + InternalErrors.Reason.ROLE_ALREADY_EXISTS, + InternalErrors.Reason.POLICY_NOT_FOUND, + InternalErrors.Reason.POLICY_NOT_FOUND_FOR_PROTECTED_RESOURCE, + InternalErrors.Reason.POLICY_ALREADY_EXISTS, + InternalErrors.Reason.POLICY_BINDING_MEMBERSHIP_ALREADY_EXISTS, + InternalErrors.Reason.POLICY_BINDING_MEMBERSHIP_NOT_FOUND, + InternalErrors.Reason.RESOURCE_TYPE_NOT_FOUND_IN_PERMISSION, + InternalErrors.Reason.REQUIRED_FIELD_NOT_SET, + InternalErrors.Reason.INVALID_FIELD_VALUE, + InternalErrors.Reason.ETAG_MISMATCH, + null -> Status.INTERNAL.withCause(e).asRuntimeException() + } + } + + return internalResponse.toPrincipal() + } } diff --git a/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/ResourceConversion.kt b/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/ResourceConversion.kt index befc81f8f31..85efda83793 100644 --- a/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/ResourceConversion.kt +++ b/src/main/kotlin/org/wfanet/measurement/access/service/v1alpha/ResourceConversion.kt @@ -1,7 +1,21 @@ +/* + * Copyright 2024 The Cross-Media Measurement Authors + * + * 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. + */ + package org.wfanet.measurement.access.service.v1alpha -import org.wfanet.measurement.internal.access.Principal as InternalPrincipal -import org.wfanet.measurement.internal.access.Role as InternalRole import org.wfanet.measurement.access.service.PermissionKey import org.wfanet.measurement.access.service.PrincipalKey import org.wfanet.measurement.access.service.RoleKey @@ -11,6 +25,9 @@ import org.wfanet.measurement.access.v1alpha.PrincipalKt.tlsClient import org.wfanet.measurement.access.v1alpha.Role import org.wfanet.measurement.access.v1alpha.principal import org.wfanet.measurement.access.v1alpha.role +import org.wfanet.measurement.internal.access.Principal as InternalPrincipal +import org.wfanet.measurement.internal.access.PrincipalKt.oAuthUser as internalOAuthUser +import org.wfanet.measurement.internal.access.Role as InternalRole fun InternalPrincipal.toPrincipal(): Principal { val source = this @@ -43,7 +60,15 @@ fun InternalRole.toRole(): Role { return role { name = RoleKey(source.roleResourceId).toName() resourceTypes += source.resourceTypesList - permissions += source.permissionResourceIdsList.map { PermissionKey(it).toName()} + permissions += source.permissionResourceIdsList.map { PermissionKey(it).toName() } etag = source.etag } } + +fun Principal.OAuthUser.toInternal(): InternalPrincipal.OAuthUser { + val source = this + return internalOAuthUser { + issuer = source.issuer + subject = source.subject + } +} diff --git a/src/main/proto/wfa/measurement/access/v1alpha/principals_service.proto b/src/main/proto/wfa/measurement/access/v1alpha/principals_service.proto index 1aa8a70c4a9..f787bac1e43 100644 --- a/src/main/proto/wfa/measurement/access/v1alpha/principals_service.proto +++ b/src/main/proto/wfa/measurement/access/v1alpha/principals_service.proto @@ -37,6 +37,9 @@ service Principals { } // Creates a `Principal`. + // + // Error reasons: + // * `PRINCIPAL_ALREADY_EXISTS` rpc CreatePrincipal(CreatePrincipalRequest) returns (Principal) { option (google.api.method_signature) = "principal,principal_id"; } diff --git a/src/test/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsServiceTest.kt b/src/test/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsServiceTest.kt index 7a5f5f038c1..e26e557c8d7 100644 --- a/src/test/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsServiceTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/access/service/v1alpha/PrincipalsServiceTest.kt @@ -34,9 +34,11 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.stub import org.wfanet.measurement.access.service.Errors +import org.wfanet.measurement.access.service.internal.PrincipalAlreadyExistsException import org.wfanet.measurement.access.service.internal.PrincipalNotFoundException import org.wfanet.measurement.access.v1alpha.GetPrincipalRequest import org.wfanet.measurement.access.v1alpha.PrincipalKt +import org.wfanet.measurement.access.v1alpha.createPrincipalRequest import org.wfanet.measurement.access.v1alpha.getPrincipalRequest import org.wfanet.measurement.access.v1alpha.principal import org.wfanet.measurement.common.grpc.errorInfo @@ -45,6 +47,7 @@ import org.wfanet.measurement.common.grpc.testing.mockService import org.wfanet.measurement.common.testing.verifyProtoArgument import org.wfanet.measurement.internal.access.PrincipalKt as InternalPrincipalKt import org.wfanet.measurement.internal.access.PrincipalsGrpcKt as InternalPrincipalsGrpcKt +import org.wfanet.measurement.internal.access.createUserPrincipalRequest as internalCreateUserPrincipalRequest import org.wfanet.measurement.internal.access.getPrincipalRequest as internalGetPrincipalRequest import org.wfanet.measurement.internal.access.principal as internalPrincipal @@ -189,4 +192,249 @@ class PrincipalsServiceTest { } ) } + + @Test + fun `createPrincipal returns user Principal`() = runBlocking { + val internalPrincipal = internalPrincipal { + principalResourceId = "user-1" + user = + InternalPrincipalKt.oAuthUser { + issuer = "example.com" + subject = "user1@example.com" + } + } + internalServiceMock.stub { + onBlocking { createUserPrincipal(any()) } doReturn internalPrincipal + } + + val request = createPrincipalRequest { + principal = principal { + name = "principals/${internalPrincipal.principalResourceId}" + user = + PrincipalKt.oAuthUser { + issuer = "example.com" + subject = "user1@example.com" + } + } + principalId = "user-1" + } + val response = service.createPrincipal(request) + + verifyProtoArgument( + internalServiceMock, + InternalPrincipalsGrpcKt.PrincipalsCoroutineImplBase::createUserPrincipal, + ) + .isEqualTo( + internalCreateUserPrincipalRequest { + principalResourceId = internalPrincipal.principalResourceId + user = internalPrincipal.user + } + ) + + assertThat(response) + .isEqualTo( + principal { + name = request.principal.name + user = + PrincipalKt.oAuthUser { + issuer = internalPrincipal.user.issuer + subject = internalPrincipal.user.subject + } + } + ) + } + + @Test + fun `createPrincipal throws PRINCIPAL_TYPE_NOT_SUPPORTED when principle identity case is TLS_CLIENT`() = + runBlocking { + val exception = + assertFailsWith { + service.createPrincipal( + createPrincipalRequest { + principal = principal { + name = "principals/user-1" + tlsClient = + PrincipalKt.tlsClient { authorityKeyIdentifier = "akid".toByteStringUtf8() } + } + } + ) + } + + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.errorInfo) + .isEqualTo( + errorInfo { + domain = Errors.DOMAIN + reason = Errors.Reason.PRINCIPAL_TYPE_NOT_SUPPORTED.name + metadata[Errors.Metadata.PRINCIPAL_TYPE.key] = "TLS_CLIENT" + } + ) + } + + @Test + fun `createPrincipal throws INVALID_FIELD_VALUE when principle user is not set`() = runBlocking { + val exception = + assertFailsWith { + service.createPrincipal( + createPrincipalRequest { + principal = principal { name = "principals/user-1" } + principalId = "user-1" + } + ) + } + + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.errorInfo) + .isEqualTo( + errorInfo { + domain = Errors.DOMAIN + reason = Errors.Reason.REQUIRED_FIELD_NOT_SET.name + metadata[Errors.Metadata.FIELD_NAME.key] = "principal.identity" + } + ) + } + + @Test + fun `createPrincipal throws REQUIRED_FIELD_NOT_SET when principal user issuer is not set`() = + runBlocking { + val exception = + assertFailsWith { + service.createPrincipal( + createPrincipalRequest { + principal = principal { + name = "principals/user-1" + user = PrincipalKt.oAuthUser { subject = "user1@example.com" } + } + principalId = "user-1" + } + ) + } + + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.errorInfo) + .isEqualTo( + errorInfo { + domain = Errors.DOMAIN + reason = Errors.Reason.REQUIRED_FIELD_NOT_SET.name + metadata[Errors.Metadata.FIELD_NAME.key] = "principal.user.issuer" + } + ) + } + + @Test + fun `createPrincipal throws REQUIRED_FIELD_NOT_SET when principal user subject is not set`() = + runBlocking { + val exception = + assertFailsWith { + service.createPrincipal( + createPrincipalRequest { + principal = principal { + name = "principals/user-1" + user = PrincipalKt.oAuthUser { issuer = "example.com" } + } + principalId = "user-1" + } + ) + } + + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.errorInfo) + .isEqualTo( + errorInfo { + domain = Errors.DOMAIN + reason = Errors.Reason.REQUIRED_FIELD_NOT_SET.name + metadata[Errors.Metadata.FIELD_NAME.key] = "principal.user.subject" + } + ) + } + + @Test + fun `createPrincipal throws REQUIRED_FIELD_NOT_SET when principle id is not set`() = runBlocking { + val exception = + assertFailsWith { + service.createPrincipal( + createPrincipalRequest { + principal = principal { + name = "principals/user-1" + user = + PrincipalKt.oAuthUser { + issuer = "example.com" + subject = "user1@example.com" + } + } + } + ) + } + + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.errorInfo) + .isEqualTo( + errorInfo { + domain = Errors.DOMAIN + reason = Errors.Reason.REQUIRED_FIELD_NOT_SET.name + metadata[Errors.Metadata.FIELD_NAME.key] = "principal_id" + } + ) + } + + @Test + fun `createPrincipal throws INVALID_FIELD_VALUE when principle id does not match RFC_1034_REGEX`() = + runBlocking { + val exception = + assertFailsWith { + service.createPrincipal( + createPrincipalRequest { + principal = principal { + name = "principals/user-1" + user = + PrincipalKt.oAuthUser { + issuer = "example.com" + subject = "user1@example.com" + } + } + principalId = "678" + } + ) + } + + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.errorInfo) + .isEqualTo( + errorInfo { + domain = Errors.DOMAIN + reason = Errors.Reason.INVALID_FIELD_VALUE.name + metadata[Errors.Metadata.FIELD_NAME.key] = "principal_id" + } + ) + } + + @Test + fun `createPrincipal throws PRINCIPAL_ALREADY_EXISTS from backend`() = runBlocking { + internalServiceMock.stub { + onBlocking { createUserPrincipal(any()) } doThrow + PrincipalAlreadyExistsException().asStatusRuntimeException(Status.Code.ALREADY_EXISTS) + } + + val request = createPrincipalRequest { + principal = principal { + name = "principals/user-1" + user = + PrincipalKt.oAuthUser { + issuer = "example.com" + subject = "user1@example.com" + } + } + principalId = "user-1" + } + val exception = assertFailsWith { service.createPrincipal(request) } + + assertThat(exception.status.code).isEqualTo(Status.Code.ALREADY_EXISTS) + assertThat(exception.errorInfo) + .isEqualTo( + errorInfo { + domain = Errors.DOMAIN + reason = Errors.Reason.PRINCIPAL_ALREADY_EXISTS.name + } + ) + } }