Skip to content

Commit

Permalink
Merge branch 'develop' into ID-1369-admin-endpoint-repair-access
Browse files Browse the repository at this point in the history
  • Loading branch information
Ghost-in-a-Jar authored Aug 28, 2024
2 parents 001fef9 + 524fbc0 commit 174c06d
Show file tree
Hide file tree
Showing 12 changed files with 824 additions and 101 deletions.
2 changes: 1 addition & 1 deletion env/local.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export ADMIN_SERVICE_ACCOUNT_4="src/main/resources/rendered/admin-service-accoun
export ADMIN_SERVICE_ACCOUNT_5="src/main/resources/rendered/admin-service-account-5.json"
export SERVICE_ACCOUNT_ADMINS="[email protected], [email protected]"
export AZURE_ENABLED="false"
export AZURE_SERVICE_CATALOG_APPS_ENABLED="false"
export AZURE_SERVICE_CATALOG_ENABLED="false"
export AZURE_MANAGED_APP_WORKLOAD_CLIENT_ID="661e243c-5ef9-4a9c-9be3-b7f5585828b3"
export EMAIL_DOMAIN="dev.test.firecloud.org"
export ENVIRONMENT="dev"
Expand Down
2 changes: 1 addition & 1 deletion env/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export AZURE_MANAGED_APP_CLIENT_SECRET="foo"
export AZURE_MANAGED_APP_TENANT_ID="foo"
export AZURE_MANAGED_APP_WORKLOAD_CLIENT_ID="foo"
export AZURE_ALLOW_MANAGED_IDENTITY_USER_CREATION="false"
export AZURE_SERVICE_CATALOG_APPS_ENABLED="false"
export AZURE_SERVICE_CATALOG_ENABLED="false"
export EMAIL_DOMAIN="dev.test.firecloud.org"
export ENVIRONMENT="local"
export GOOGLE_APPS_DOMAIN="test.firecloud.org"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class SamProviderSpec
when {
googleExt.getArbitraryPetServiceAccountToken(any[SamUser], any[Set[String]], any[SamRequestContext])
} thenReturn {
Future.successful("aToken")
IO.pure("aToken")
}
)
} yield ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@
<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/20240809_sam_user_favorite_resources_table.xml" relativeToChangelogFile="true"/>
<include file="changesets/20240820_descendant_auth_domains.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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="dvoet" id="populate_descendant_auth_domains" runInTransaction="true">
<sql stripComments="true">
with recursive
ancestor_resource(resource_id, group_id) as (
select ad.resource_id, ad.group_id
from sam_resource_auth_domain ad
union
select childResource.id, ancestorResource.group_id
from SAM_RESOURCE childResource
join ancestor_resource ancestorResource on ancestorResource.resource_id = childResource.resource_parent_id
)

insert into sam_resource_auth_domain(resource_id, group_id)
select ar.resource_id, ar.group_id
from ancestor_resource ar
on conflict do nothing
</sql>
</changeSet>

</databaseChangeLog>
110 changes: 107 additions & 3 deletions src/main/resources/swagger/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
/api/google/v1/petServiceAccount/{project}/{email}/token:
post:
tags:
- Google
summary: gets a token for the user's pet service account, get_pet_private_key
action on cloud-extension/google required
operationId: getUserPetServiceAccountToken
parameters:
- name: project
in: path
description: Google project of the pet
required: true
schema:
type: string
- name: email
in: path
description: User's email address
required: true
schema:
type: string
requestBody:
description: Scopes for the token
content:
'application/json':
schema:
$ref: '#/components/schemas/ArrayOfScopes'
required: true
responses:
200:
description: an access token for the users pet service account
content:
application/json:
schema:
type: string
403:
description: caller has some access to cloud-extension/google but not to
the get_pet_private_key action
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
404:
description: user does not exist or caller does not have any access to cloud-extension/google
resource
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
x-codegen-request-body-name: scopes
/api/google/v1/petServiceAccount/{email}/key:
get:
tags:
Expand Down Expand Up @@ -1149,6 +1204,55 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
/api/google/v1/petServiceAccount/{email}/token:
post:
tags:
- Google
summary: gets a token for the user's arbitrary pet service account, get_pet_private_key
action on cloud-extension/google required
operationId: getUserArbitraryPetServiceAccountToken
parameters:
- name: email
in: path
description: User's email address
required: true
schema:
type: string
requestBody:
description: Scopes for the token
content:
'application/json':
schema:
$ref: '#/components/schemas/ArrayOfScopes'
required: true
responses:
200:
description: an access token for the users pet service account
content:
application/json:
schema:
type: string
403:
description: caller has some access to cloud-extension/google but not to
the get_pet_private_key action
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
404:
description: user does not exist or caller does not have any access to cloud-extension/google
resource
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
500:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
x-codegen-request-body-name: scopes
/api/google/v1/user/signedUrlForBlob:
post:
tags:
Expand Down Expand Up @@ -1802,8 +1906,8 @@ paths:
tags:
- Resources
summary: List resources the calling user has been granted policies/roles/actions on.
This endpoint MUST NOT be used to determine if a user has an action on a resource.
This endpoint is ONLY for determining the state of the user's permissions.
This endpoint MUST NOT be used to determine if a user has an action on a resource.
This endpoint is ONLY for determining the state of the user's permissions.
It does not take into account enabled status, or Terms of Service Compliance, nor does it take into account Auth Domains.
To check if a user is allowed to execute an action on a resource, use /api/resources/v2/{resourceTypeName}/{resourceId}/action/{action}
By default, public resources are not included. Including public resources incurs a 3x cost of DB performance.
Expand Down Expand Up @@ -4518,7 +4622,7 @@ components:
type: boolean
description: true if there is a rolling acceptance window active. This
means that users who have not accepted the latest terms of service
version (and who have accepted the previous version) will be allowed to use the system
version (and who have accepted the previous version) will be allowed to use the system
for a period of time after the terms of service have been updated.
UserTermsOfServiceDetails:
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM
patchWithTelemetry(samRequestContext, resourceParams(resource): _*) {
requireAction(resource, SamResourceActions.updateAuthDomain, samUser.id, samRequestContext) {
entity(as[Set[WorkbenchGroupName]]) { authDomains =>
complete(resourceService.addResourceAuthDomain(resource, authDomains, samUser.id, samRequestContext).map { response =>
complete(resourceService.addResourceAuthDomain(resource, authDomains, Option(samUser.id), samRequestContext).map { response =>
StatusCodes.OK -> response
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,34 +42,64 @@ trait GoogleExtensionRoutes extends ExtensionRoutes with SamUserDirectives with
samUser.id,
samRequestContext
) {
path(Segment / "key") { userEmail =>
pathPrefix(Segment) { userEmail =>
val email = WorkbenchEmail(userEmail)
getWithTelemetry(samRequestContext, "userEmail" -> email) {
complete {
import spray.json._
googleExtensions.getArbitraryPetServiceAccountKey(email, samRequestContext) map {
// parse json to ensure it is json and tells akka http the right content-type
case Some(key) => StatusCodes.OK -> key.parseJson
case None =>
throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, "pet service account not found"))
}
}
}
} ~
path(Segment / Segment) { (project, userEmail) =>
val email = WorkbenchEmail(userEmail)
val googleProject = GoogleProject(project)
getWithTelemetry(samRequestContext, "userEmail" -> email, "googleProject" -> googleProject) {
path("key") {
getWithTelemetry(samRequestContext, "userEmail" -> email) {
complete {
import spray.json._
googleExtensions.getPetServiceAccountKey(email, googleProject, samRequestContext) map {
googleExtensions.getArbitraryPetServiceAccountKey(email, samRequestContext) map {
// parse json to ensure it is json and tells akka http the right content-type
case Some(key) => StatusCodes.OK -> key.parseJson
case None =>
throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, "pet service account not found"))
}
}
}
} ~
path("token") {
postWithTelemetry(samRequestContext, "userEmail" -> email) {
entity(as[Set[String]]) { scopes =>
complete {
googleExtensions.getArbitraryPetServiceAccountToken(email, scopes, samRequestContext).map {
case Some(token) => StatusCodes.OK -> JsString(token)
case None =>
throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, "pet service account not found"))
}
}
}
}
}
} ~
pathPrefix(Segment / Segment) { (project, userEmail) =>
val email = WorkbenchEmail(userEmail)
val googleProject = GoogleProject(project)
pathEndOrSingleSlash {
getWithTelemetry(samRequestContext, "userEmail" -> email, "googleProject" -> googleProject) {
complete {
import spray.json._
googleExtensions.getPetServiceAccountKey(email, googleProject, samRequestContext) map {
// parse json to ensure it is json and tells akka http the right content-type
case Some(key) => StatusCodes.OK -> key.parseJson
case None =>
throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, "pet service account not found"))
}
}
}
} ~
path("token") {
postWithTelemetry(samRequestContext, "userEmail" -> email) {
entity(as[Set[String]]) { scopes =>
complete {
googleExtensions.getPetServiceAccountToken(email, googleProject, scopes, samRequestContext).map {
case Some(token) => StatusCodes.OK -> JsString(token)
case None =>
throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, "pet service account not found"))
}
}
}
}
}
}
}
} ~
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,30 +455,55 @@ class GoogleExtensions(
key <- googleKeyCache.getKey(pet)
} yield key

def getPetServiceAccountToken(user: SamUser, project: GoogleProject, scopes: Set[String], samRequestContext: SamRequestContext): Future[String] =
getPetServiceAccountKey(user, project, samRequestContext).unsafeToFuture().flatMap { key =>
getAccessTokenUsingJson(key, scopes)
def getPetServiceAccountToken(user: SamUser, project: GoogleProject, scopes: Set[String], samRequestContext: SamRequestContext): IO[String] =
getPetServiceAccountKey(user, project, samRequestContext).flatMap { key =>
IO.fromFuture(IO(getAccessTokenUsingJson(key, scopes)))
}

def getPetServiceAccountToken(
userEmail: WorkbenchEmail,
project: GoogleProject,
scopes: Set[String],
samRequestContext: SamRequestContext
): IO[Option[String]] =
for {
subject <- directoryDAO.loadSubjectFromEmail(userEmail, samRequestContext)
token <- subject match {
case Some(userId: WorkbenchUserId) =>
getPetServiceAccountToken(SamUser(userId, None, userEmail, None, false), project, scopes, samRequestContext).map(Option(_))
case _ => IO.pure(None)
}
} yield token

def getArbitraryPetServiceAccountKey(userEmail: WorkbenchEmail, samRequestContext: SamRequestContext): IO[Option[String]] =
for {
subject <- directoryDAO.loadSubjectFromEmail(userEmail, samRequestContext)
key <- subject match {
case Some(userId: WorkbenchUserId) =>
IO.fromFuture(IO(getArbitraryPetServiceAccountKey(SamUser(userId, None, userEmail, None, false), samRequestContext))).map(Option(_))
getArbitraryPetServiceAccountKey(SamUser(userId, None, userEmail, None, false), samRequestContext).map(Option(_))
case _ => IO.none
}
} yield key

def getArbitraryPetServiceAccountKey(user: SamUser, samRequestContext: SamRequestContext): Future[String] =
getDefaultServiceAccountForShellProject(user, samRequestContext)
def getArbitraryPetServiceAccountKey(user: SamUser, samRequestContext: SamRequestContext): IO[String] =
IO.fromFuture(IO(getDefaultServiceAccountForShellProject(user, samRequestContext)))

def getArbitraryPetServiceAccountToken(userEmail: WorkbenchEmail, scopes: Set[String], samRequestContext: SamRequestContext): IO[Option[String]] =
for {
subject <- directoryDAO.loadSubjectFromEmail(userEmail, samRequestContext)
token <- subject match {
case Some(userId: WorkbenchUserId) =>
getArbitraryPetServiceAccountToken(SamUser(userId, None, userEmail, None, false), scopes, samRequestContext).map(Option(_))
case _ => IO.none
}
} yield token

def getArbitraryPetServiceAccountToken(user: SamUser, scopes: Set[String], samRequestContext: SamRequestContext): Future[String] =
def getArbitraryPetServiceAccountToken(user: SamUser, scopes: Set[String], samRequestContext: SamRequestContext): IO[String] =
getArbitraryPetServiceAccountKey(user, samRequestContext).flatMap { key =>
getAccessTokenUsingJson(key, scopes)
IO.fromFuture(IO(getAccessTokenUsingJson(key, scopes)))
}

private def getDefaultServiceAccountForShellProject(user: SamUser, samRequestContext: SamRequestContext): Future[String] = {
private[google] def getDefaultServiceAccountForShellProject(user: SamUser, samRequestContext: SamRequestContext): Future[String] = {
val projectName =
s"fc-${googleServicesConfig.environment.substring(0, Math.min(googleServicesConfig.environment.length(), 5))}-${user.id.value}" // max 30 characters. subject ID is 21
for {
Expand Down Expand Up @@ -690,7 +715,7 @@ class GoogleExtensions(
val bucket = GcsBucketName(blobId.getBucket)
val objectName = GcsBlobName(blobId.getName)
for {
petKey <- IO.fromFuture(IO(getArbitraryPetServiceAccountKey(samUser, samRequestContext)))
petKey <- getArbitraryPetServiceAccountKey(samUser, samRequestContext)
serviceAccountCredentials = ServiceAccountCredentials.fromStream(new ByteArrayInputStream(petKey.getBytes()))
url <- getSignedUrl(samUser, bucket, objectName, duration, urlParamsMap, serviceAccountCredentials)
} yield url
Expand Down
Loading

0 comments on commit 174c06d

Please sign in to comment.