Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Action Service Accounts Proof of Concept #1321

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions env/local.env
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export LEONARDO_PET_SERVICE_ACCOUNT="leonardo-dev@broad-dsde-dev.iam.gserviceacc
export OIDC_AUTHORITY_ENDPOINT="https://terradevb2c.b2clogin.com/terradevb2c.onmicrosoft.com/v2.0?p=b2c_1a_signup_signin_dev"
export GOOGLE_TRACE_SAMPLING_PROBABILITY=0
export GOOGLE_TRACE_ENABLED="false"
export PET_SIGNING_ACCOUNTS_ENABLED="true"
export POSTGRES_PASSWORD="sam-test"
export POSTGRES_READ_URL="jdbc:postgresql://localhost:5432/testdb"
export POSTGRES_WRITE_URL="jdbc:postgresql://localhost:5432/testdb"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ object MockTestSupport extends MockTestSupport {
policyDAO,
googleExt,
FakeOpenIDConnectConfiguration,
azureService:Option[AzureService]
azureService: Option[AzureService]
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import org.broadinstitute.dsde.workbench.sam.MockTestSupport.genSamRoutes
import org.broadinstitute.dsde.workbench.sam.api.StandardSamUserDirectives
import org.broadinstitute.dsde.workbench.sam.azure.AzureService
import org.broadinstitute.dsde.workbench.sam.dataAccess.{AccessPolicyDAO, DirectoryDAO, StatefulMockAccessPolicyDaoBuilder}
import org.broadinstitute.dsde.workbench.sam.google.GoogleExtensions
import org.broadinstitute.dsde.workbench.sam.google.{GoogleExtensions, PetServiceAccounts}
import org.broadinstitute.dsde.workbench.sam.model._
import org.broadinstitute.dsde.workbench.sam.model.api.SamUser
import org.broadinstitute.dsde.workbench.sam.service._
Expand Down Expand Up @@ -95,6 +95,7 @@ class SamProviderSpec

// The following services are mocked for now
val googleExt: GoogleExtensions = mock[GoogleExtensions]
val mockPetServiceAccountExt = mock[PetServiceAccounts]
val mockManagedGroupService: ManagedGroupService = mock[ManagedGroupService]
val tosService: TosService = MockTosServiceBuilder().withAllAccepted().build
val azureService: AzureService = mock[AzureService]
Expand Down Expand Up @@ -135,11 +136,18 @@ class SamProviderSpec
private def mockGetArbitraryPetServiceAccountToken(): IO[Unit] = for {
_ <- IO(
when {
googleExt.getArbitraryPetServiceAccountToken(any[SamUser], any[Set[String]], any[SamRequestContext])
mockPetServiceAccountExt.getArbitraryPetServiceAccountToken(any[SamUser], any[Set[String]], any[SamRequestContext])
} thenReturn {
Future.successful("aToken")
}
)
_ <- IO(
when {
googleExt.petServiceAccounts
} thenReturn {
mockPetServiceAccountExt
}
)
} yield ()

private def mockResourceActionPermission(action: ResourceAction, hasPermission: Boolean): IO[Unit] = for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@
<include file="changesets/20240417_action_managed_identities.xml" relativeToChangelogFile="true"/>
<include file="changesets/20240416_add_sam_rac_tables.xml" relativeToChangelogFile="true"/>
<include file="changesets/20240701_add_group_version_and_last_synchronized_version.xml" relativeToChangelogFile="true"/>
<include file="changesets/20240812_action_service_accounts.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog logicalFilePath="dummy"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">

<changeSet logicalFilePath="dummy" author="tlangs" id="action_service_accounts">
<createTable tableName="SAM_ACTION_SERVICE_ACCOUNT">
<column name="resource_id" type="BIGINT">
<constraints nullable="false" primaryKey="true" foreignKeyName="FK_SASA_RESOURCE" referencedTableName="SAM_RESOURCE" referencedColumnNames="id" deleteCascade="true"/>
</column>

<column name="resource_action_id" type="BIGINT">
<constraints nullable="false" primaryKey="true" foreignKeyName="FK_SASA_ACTION" referencedTableName="SAM_RESOURCE_ACTION" referencedColumnNames="id"/>
</column>
<column name="project" type="VARCHAR">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="email" type="VARCHAR">
<constraints nullable="false" unique="true"/>
</column>
<column name="google_subject_id" type="VARCHAR(30)">
<constraints unique="true" nullable="false"/>
</column>
<column name="display_name" type="VARCHAR(100)">
<constraints unique="false" nullable="false"/>
</column>
</createTable>
<createTable tableName="SAM_PET_SIGNING_ACCOUNT">
<column name="sam_user_id" type="VARCHAR">
<constraints primaryKey="true" foreignKeyName="FK_PET_SIGN_ACCT" referencedTableName="SAM_USER" referencedColumnNames="id" deleteCascade="true"/>
</column>
<column name="project" type="VARCHAR">
<constraints primaryKey="true"/>
</column>
<column name="email" type="VARCHAR">
<constraints nullable="false" unique="true"/>
</column>
<column name="google_subject_id" type="VARCHAR(30)">
<constraints unique="true" nullable="false"/>
</column>
<column name="display_name" type="VARCHAR(100)">
<constraints unique="false" nullable="false"/>
</column>
</createTable>
<createTable tableName="SAM_USER_GOOGLE_PROJECT">
<column name="sam_user_id" type="VARCHAR">
<constraints primaryKey="true" foreignKeyName="FK_SUGP" referencedTableName="SAM_USER" referencedColumnNames="id"/>
</column>
<column name="project" type="VARCHAR">
<constraints primaryKey="true"/>
</column>
</createTable>
</changeSet>

</databaseChangeLog>
1 change: 1 addition & 0 deletions src/main/resources/sam.conf
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ googleServices {
samplingProbability = ${?OPENCENSUS_SAMPLING_PROBABILITY} # for backwards compatibility
samplingProbability = ${?GOOGLE_TRACE_SAMPLING_PROBABILITY}
}
petSigningAccountsEnabled = ${?PET_SIGNING_ACCOUNTS_ENABLED}
}

db {
Expand Down
153 changes: 118 additions & 35 deletions src/main/resources/swagger/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1331,41 +1331,6 @@ paths:
schema:
$ref: '#/components/schemas/ErrorReport'
x-codegen-request-body-name: scopes
/api/google/v1/user/petServiceAccount/{project}/signedUrlForBlob:
post:
tags:
- Google
summary: Gets a signed URL for the given blob, signed by the Pet Service account of the calling user.
The signed URL is active for 1 hour and scoped to the permissions of the signing Pet Service Account.
Sam will provide a signed URL for any object path, even if that object does not exist.
operationId: getSignedUrlForBlob
parameters:
- name: project
in: path
description: Google project of the pet
required: true
schema:
type: string
requestBody:
description: bucketName and blobName of the object to get a signed URL for
content:
'application/json':
schema:
$ref: '#/components/schemas/SignedUrlRequest'
required: true
responses:
200:
description: signed URL for the blob, signed by the Pet Service Account key
content:
application/json:
schema:
type: string
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
/api/google/v1/user/proxyGroup/{email}:
get:
tags:
Expand Down Expand Up @@ -1477,6 +1442,109 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
/api/google/v2/actionServiceAccount/{project}/{resourceTypeName}/{resourceId}/{action}:
post:
tags:
- Google
summary: Gets/creates an Action Service Account in a Google Project for an action on a resource
operationId: getActionServiceAccount
parameters:
- name: project
in: path
description: Google project of the the resource
required: true
schema:
type: string
- name: resourceTypeName
in: path
description: Type of resource
required: true
schema:
type: string
- name: resourceId
in: path
description: Id of resource
required: true
schema:
type: string
- name: action
in: path
description: Name of action to create an Action Service Account for
required: true
schema:
type: string
requestBody:
content:
'application/json':
schema:
type: object
responses:
200:
description: Successfully created Action Service Account
content:
text/plain:
schema:
type: string
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
/api/google/v2/actionServiceAccount/{project}/{resourceTypeName}/{resourceId}/{action}/signedUrlForBlob:
post:
tags:
- Google
summary: Gets a signed URL for the given blob, signed by the Action Service Account for the given
action, delegated to by the user's Pet Signing Account
The signed URL is active for 1 hour and scoped to the permissions of the Action Service Account.
Sam will provide a signed URL for any object path, even if that object does not exist.
operationId: getActionServiceAccountSignedUrlForBlob
parameters:
- name: project
in: path
description: project of the GCP Resource
required: true
schema:
type: string
- name: resourceTypeName
in: path
description: Resource type name
required: true
schema:
type: string
- name: resourceId
in: path
description: Resource ID
required: true
schema:
type: string
- name: action
in: path
description: Action on the Resource Type to use
required: true
schema:
type: string
requestBody:
description: gspath of the object to get a signed URL for
content:
'application/json':
schema:
$ref: '#/components/schemas/RequesterPaysSignedUrlRequest'
required: true
responses:
200:
description: signed URL for the blob, signed by the Pet Signing Account key via the Action Service Account
content:
application/json:
schema:
type: string
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
/api/azure/v1/billingProfile/{billingProfileId}/managedResourceGroup:
post:
tags:
Expand Down Expand Up @@ -3895,6 +3963,21 @@ components:
requesterPaysProject:
type: string
description: Optional Google Project to use for billing.
ActionServiceAccountSignedUrlRequest:
type: object
required:
- gsPath
properties:
gsPath:
type: string
description: GS Path to the blob
duration:
type: number
description: Optional validity duration of the link in minutes. Defaults to 1 hour.
default: 60
requesterPaysProject:
type: string
description: Optional Google Project to use for billing.
CreateResourceRequest:
required:
- policies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ final case class GoogleServicesConfig(
adminSdkServiceAccountPaths: Option[NonEmptyList[String]],
googleKms: GoogleKmsConfig,
terraGoogleOrgNumber: String,
traceExporter: TraceExporterConfig
traceExporter: TraceExporterConfig,
petSigningAccountsEnabled: Boolean
)

object GoogleServicesConfig {
Expand Down Expand Up @@ -98,7 +99,8 @@ object GoogleServicesConfig {
config.as[Option[NonEmptyList[String]]]("adminSdkServiceAccountPaths"),
config.as[GoogleKmsConfig]("kms"),
config.getString("terraGoogleOrgNumber"),
config.as[TraceExporterConfig]("traceExporter")
config.as[TraceExporterConfig]("traceExporter"),
config.as[Option[Boolean]]("petSigningAccountsEnabled").getOrElse(false)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import org.broadinstitute.dsde.workbench.sam.azure.{
}
import org.broadinstitute.dsde.workbench.sam.model.api.{AdminUpdateUserRequest, SamUser, SamUserAttributes}
import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, FullyQualifiedResourceId, ResourceAction, SamUserTos}
import org.broadinstitute.dsde.workbench.sam.model.api.{ActionServiceAccount, ActionServiceAccountId}
import org.broadinstitute.dsde.workbench.sam.model.ResourceId
import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext

import java.time.Instant
Expand Down Expand Up @@ -116,6 +118,31 @@ trait DirectoryDAO {

def updatePetServiceAccount(petServiceAccount: PetServiceAccount, samRequestContext: SamRequestContext): IO[PetServiceAccount]

def createActionServiceAccount(actionServiceAccount: ActionServiceAccount, samRequestContext: SamRequestContext): IO[ActionServiceAccount]

def loadActionServiceAccount(actionServiceAccountId: ActionServiceAccountId, samRequestContext: SamRequestContext): IO[Option[ActionServiceAccount]]

def updateActionServiceAccount(actionServiceAccount: ActionServiceAccount, samRequestContext: SamRequestContext): IO[ActionServiceAccount]

def deleteActionServiceAccount(actionServiceAccountId: ActionServiceAccountId, samRequestContext: SamRequestContext): IO[Unit]

def getAllActionServiceAccountsForResource(
resourceId: ResourceId,
samRequestContext: SamRequestContext
): IO[Seq[ActionServiceAccount]]

def deleteAllActionServiceAccountsForResource(resourceId: ResourceId, samRequestContext: SamRequestContext): IO[Unit]

def createPetSigningAccount(petServiceAccount: PetServiceAccount, samRequestContext: SamRequestContext): IO[PetServiceAccount]

def loadPetSigningAccount(petServiceAccountId: PetServiceAccountId, samRequestContext: SamRequestContext): IO[Option[PetServiceAccount]]

def updatePetSigningAccount(petServiceAccount: PetServiceAccount, samRequestContext: SamRequestContext): IO[PetServiceAccount]

def loadUserPetSigningAccount(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Option[PetServiceAccount]]

def deletePetSigningAccount(petServiceAccountId: PetServiceAccountId, samRequestContext: SamRequestContext): IO[Unit]

def getManagedGroupAccessInstructions(groupName: WorkbenchGroupName, samRequestContext: SamRequestContext): IO[Option[String]]

def setManagedGroupAccessInstructions(groupName: WorkbenchGroupName, accessInstructions: String, samRequestContext: SamRequestContext): IO[Unit]
Expand Down Expand Up @@ -174,7 +201,6 @@ trait DirectoryDAO {
def setUserRegisteredAt(userId: WorkbenchUserId, registeredAt: Instant, samRequestContext: SamRequestContext): IO[Unit]

def getUserAttributes(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Option[SamUserAttributes]]

def setUserAttributes(samUserAttributes: SamUserAttributes, samRequestContext: SamRequestContext): IO[Unit]

def listParentGroups(groupName: WorkbenchGroupName, samRequestContext: SamRequestContext): IO[Set[WorkbenchGroupName]]
Expand Down
Loading
Loading