Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
224 commits
Select commit Hold shift + click to select a range
b62fba7
Nerf the thrall migration thread; very noisy.
tonytw1 Jun 4, 2024
b7bd7b2
Delete SyncChecker; not needed yet
tonytw1 May 18, 2024
6cb02e6
Delete Scripts; not needed yet and difficult.
tonytw1 May 18, 2024
81b2e66
Delete GoodToGoCheck; not instance specific.
tonytw1 Jun 25, 2024
e1c0606
Introduce an Instance model class and a way to infer it from a request.
tonytw1 Aug 29, 2024
3dc7803
!!!!!!!!!!!!!!!!! Start of multi tenant - Redefine Service.apiUrl to …
tonytw1 Nov 23, 2024
bd96385
Catch all .rootUri usages in s strings in media-api. Media API will p…
tonytw1 Aug 29, 2024
c5068b1
KahunaController will need to evaluate the service URLs to pass to th…
tonytw1 May 14, 2024
b23f3b7
imgproxy is vhost based.
tonytw1 Aug 29, 2024
41f9e1c
cropper and metadata-editor are vhost based.
tonytw1 Aug 29, 2024
829d8bf
loader, projection, usages, collections and leases move to vhost urls.
tonytw1 Nov 23, 2024
b9f90ff
auth, kahuna and thrall move to vhost url. Stops at extends Panda. We…
tonytw1 Aug 29, 2024
029cb71
Switch service names to instance aware instance.domain vhost names.
tonytw1 May 19, 2024
e3e82a1
Marking TODO corsFilter and securityHeadersFilter will need to be rew…
tonytw1 May 16, 2024
01eaccf
Reinstate security headers using an instance specific config.
tonytw1 Aug 10, 2024
4ca19b5
Implement instance specific CORS filter.
tonytw1 Aug 12, 2024
714c198
Reinstate CSRF filter with needed the CORS filter to single bypass on…
tonytw1 Aug 12, 2024
f83df14
Initial instance awareness. Prepare upload records a request specific…
tonytw1 Mar 14, 2026
585c615
Message instance field moves up from ImageUpdate to all ExternalThral…
tonytw1 Aug 29, 2024
655d096
Media API's ElasticSearch class accepts instances inputs into current…
tonytw1 Aug 29, 2024
f1957a8
Media API's Elastic responses are probably instance specific.
tonytw1 May 4, 2026
2509ff5
Step back to service to service calls via the full public URLs so tha…
tonytw1 Sep 8, 2024
73f6741
Introduce a typed Instance and pass it around implicitly.
tonytw1 Nov 23, 2024
6293832
Kahuna auth uri in main.view are correct.
tonytw1 Aug 30, 2024
dc7ce6c
Patch instance url s"" usages with toString.
tonytw1 May 19, 2024
7dbcf37
URIs take a full Instance rather than Request; use more implicits so …
tonytw1 Nov 10, 2024
50ba6f5
Crop paths are prefixed with instance id like originals and thumbs.
tonytw1 Aug 29, 2024
2629594
Collections store instance aware using Dynamo composite keys on id + …
tonytw1 May 19, 2024
687cdae
Instance aware all collections end points.
tonytw1 May 19, 2024
01de869
ImageCollectionsStore is instance aware.
tonytw1 Feb 26, 2026
9e3968c
Add Instance aware dynamnodb
tonytw1 May 19, 2024
23a7346
Instance aware all collections can scan all by querying on the instan…
tonytw1 Feb 26, 2026
c88dade
Instance aware all scanForId is instances aware.
tonytw1 Feb 26, 2026
c289a45
batchGet query is instance aware.
tonytw1 Apr 19, 2025
fbcb3a3
InstanceAwareDynamoDB getV2, booleanGetV2 and setGetV2 methods.
tonytw1 Mar 14, 2026
71ba239
InstanceAwareDynamoDB asJsObject strips instance key.
tonytw1 Mar 14, 2026
2ce4e04
InstanceAwareDynamoDB - use InstanceKey every where.
tonytw1 Mar 14, 2026
1c69098
InstanceAwareDynamoDB removeKeyV2.
tonytw1 Mar 18, 2026
6d884b0
InstanceAwareDynamoDB booleanSetOrRemoveV2.
tonytw1 Mar 18, 2026
db5c2e9
InstanceAwareDynamoDB setAddV2setAddV2 and setDeleteV2.
tonytw1 Mar 19, 2026
c32b373
InstanceAwareDynamoDB jsonAddV2.
tonytw1 Mar 19, 2026
0587beb
InstanceAwareDynamoDB deleteItem no instance regression.
tonytw1 Mar 20, 2026
1594c2a
InstanceAwareDynamoDB deleteItemV2 and stringSetV2.
tonytw1 Mar 19, 2026
d982e9f
InstanceAwareDynamoDB deleting v1 methods.
tonytw1 Mar 20, 2026
fe46e9e
InstanceAwareDynamoDB removing unused v1 methods.
tonytw1 May 23, 2025
704cc65
InstanceAwareDynamoDB removing unused v1 methods.
tonytw1 Mar 21, 2026
ed2c3d5
InstanceAwareDynamoDB purge v1 methods.
tonytw1 Mar 28, 2026
3507259
InstanceAwareDynamoDB deleteItem unused.
tonytw1 Mar 20, 2026
91571c5
InstanceAwareDynamoDB deleteItemV2 is instance aware.
tonytw1 Mar 20, 2026
71da4a3
InstanceAwareDynamoDB scanByIdV2
tonytw1 Mar 28, 2026
7e8e1e7
InstanceAwareDynamoDB batchGetV2
tonytw1 Mar 28, 2026
c089995
Meta data edits and syndication dynamo calls move to instance aware.
tonytw1 Mar 20, 2026
0dcb2aa
EditsController imports.
tonytw1 Mar 14, 2026
cdcb6a6
Move Leases to id + instance composite key; Scanomo had to be by past…
tonytw1 Nov 23, 2024
39cc6cb
LeaseStoreSpec is instance aware.
tonytw1 Mar 17, 2026
f4e1ee2
Upload status table reads are instance aware.
tonytw1 Nov 10, 2024
e888be9
Patch up soft deletes table best we can given that Scanmomo doesn't s…
tonytw1 Nov 10, 2024
6ed9a97
Just stick instance in soft delete images; not that public facing.
tonytw1 Nov 10, 2024
905bb06
Begin integrating the auth provider Kinde.
tonytw1 May 21, 2024
872862d
Logout by clearing play session.
tonytw1 May 21, 2024
0903eed
Redirect out of auth.
tonytw1 May 22, 2024
dd6d2bf
/logout link needs a slash in this setup.
tonytw1 May 22, 2024
1c68ac1
Use seperate cookie (not Play session) so it be copied for internal c…
tonytw1 May 22, 2024
1d26073
Flush cookie.
tonytw1 May 22, 2024
01ec508
Logout from Kinde on logout.
tonytw1 May 22, 2024
6212661
Auth can run at the root of the domain to auth all instances.
tonytw1 May 22, 2024
2e94eb9
Default exit to /
tonytw1 May 22, 2024
326d4b0
auth cookie not been seen on subdomains without an explicit domain set!?
tonytw1 May 22, 2024
8f01b66
Redirect logout back to home page.
tonytw1 May 22, 2024
4937e2a
/auth/session needs to be on the same host cause CORS.
tonytw1 May 22, 2024
bcb827f
Capture loggedin user cookie for use with onBehalfOf like Panda.
tonytw1 May 22, 2024
ad8ef31
/auth/session needs to be on the same host cause CORS.
tonytw1 May 22, 2024
f4c3726
Fix share URL is double //
tonytw1 May 22, 2024
5739520
Sign Kinde logged in user cookies.
tonytw1 May 23, 2024
fadad5e
Capture all Kinde user details.
tonytw1 May 23, 2024
75c95aa
Resolved TODO; random session backed state for Kinde oauth dance.
tonytw1 May 25, 2024
057c0ec
Auth decorates authed user cookie with their instances on oauth callb…
tonytw1 May 30, 2024
27d361c
Auth provider decorates Principal with allowed instances. Definition …
tonytw1 Jun 1, 2024
38bcb69
Use domainRoot is the as the config hook to remove hard coded griddev…
tonytw1 Jun 3, 2024
2fa3374
Inline Await and timeout on Kinde request.
tonytw1 Jun 4, 2024
bafaa6c
Allow InnerServicePrincipal to access all instances.
tonytw1 Jun 6, 2024
f0e496e
Setup for non instance principals instances lookup.
tonytw1 Jun 6, 2024
8cc4136
Auth calls back to landing service for users instances instead of rea…
tonytw1 Jun 6, 2024
32abd44
Quiet logging.
tonytw1 Jun 10, 2024
baf87aa
Clean up; useless future.
tonytw1 Oct 26, 2024
23d04df
Introduce CreateInstanceMessage so that thrall can respond to an on d…
tonytw1 May 28, 2024
8e4a828
CreateInstance is added to ExternalThrallMessage and passes through t…
tonytw1 Nov 23, 2024
ba230e7
Thrall create instance calls collections to create the root Home coll…
tonytw1 Aug 30, 2024
7482edf
Test does not been an ElasticSearch base.
tonytw1 May 29, 2024
0f41280
Logging around new instance root collection setup.
tonytw1 Jun 6, 2024
91fd256
Marking TODO - Usages not instance aware.
tonytw1 Jun 7, 2024
29e8886
Sign cropper assets URLs.
tonytw1 Jun 7, 2024
e5c262b
Delete crops is instance aware.
tonytw1 Dec 3, 2024
aedd528
Need to fws instance aware update notifications from usage API.
tonytw1 Jun 10, 2024
f0b0802
Spike; progating instance throught the RX? channels for simple Usage …
tonytw1 Jun 10, 2024
4acc1a5
WIP pushing instance down to Usages table for instance aware queries.
tonytw1 Mar 13, 2026
1305c57
Setting up from instance specific usage table.
tonytw1 Jun 29, 2024
ab2ed69
Prefix usage groping with instance slash to make filterable by instance.
tonytw1 Jun 29, 2024
1b3cc09
UsageRecord persists an instance field so that it can be used as the …
tonytw1 Jun 29, 2024
d53970f
UsageTableTest instance aware tests.
tonytw1 Mar 15, 2026
469c32f
UsageTableTest implicits.
tonytw1 Mar 21, 2026
65ca3b0
Thrall polls for list of current instances; setting up for collection…
tonytw1 Jun 15, 2024
2a47bf5
Thrall counts total ES images foreach instance.
tonytw1 Jun 15, 2024
4594e75
Thrall knows image count and total file size for instances.
tonytw1 Jun 25, 2024
4a42e46
Thrall announces instance usage onto an SQS queue for instances to pi…
tonytw1 Nov 23, 2024
656def2
Thralls call back to instances endpoint is conf.
tonytw1 Jun 16, 2024
ec11b69
My instances endpoint for auth moves to config.
tonytw1 Jun 21, 2024
64e1dbe
Thrall can ensure index on start up to help rebuild Elastic.
tonytw1 Jun 22, 2024
7eea1e2
Push instance all the way down to fileKeyFromId to catch all usages.
tonytw1 Nov 10, 2024
1ece6d0
Instance specific Reaper
tonytw1 Jun 25, 2024
c01ed7b
Reaper can run without logging to bucket.
tonytw1 Jun 25, 2024
769780d
Clean up; logging.
tonytw1 Jun 29, 2024
3174f9b
Clean up; extract method.
tonytw1 Jul 12, 2024
7073149
Renable image-deleted messages.
tonytw1 Sep 5, 2024
bf5b606
Restore image-deleted messages and make instance specific.
tonytw1 Sep 5, 2024
6495496
Marking TODO; no image-deleted notification after hard reap?
tonytw1 Nov 10, 2024
942606d
Ingest images from instance specific top level folders in the ingest …
tonytw1 Feb 28, 2026
d34500c
fileKeyFromId uses implicit Instance for smaller diff.
tonytw1 Aug 9, 2025
bc446c7
optimisedPngKeyFromId uses implicit Instance for smaller diff.
tonytw1 Aug 9, 2025
c1eed3b
Log principal for blocked request.
tonytw1 Sep 10, 2024
8144c3d
Enable API key access to ownered instances. Request owner instances b…
tonytw1 Sep 10, 2024
cab9dee
Remove deprecated TODO
tonytw1 Nov 7, 2024
52e3a41
Clean up.
tonytw1 Nov 7, 2024
cac89dc
Instance usage message adds softDeletedCount field.
tonytw1 Sep 23, 2024
fe3b694
Logging.
tonytw1 Sep 23, 2024
58e2e9b
Refactor; extract instance message sender.
tonytw1 Sep 25, 2024
5aa2d9f
Instance usage JSON writes.
tonytw1 Sep 25, 2024
6c198e3
Setting up to send instance message after setup has completed; SQS is…
tonytw1 Oct 4, 2024
8590821
Multiplex instance related messages.
tonytw1 Sep 25, 2024
0a31837
Reaper query build logging.
tonytw1 Sep 26, 2024
0bdb9c1
Reaper controller logging.
tonytw1 Sep 26, 2024
f4b3461
KeyStore seperates API keys by instance folder.
tonytw1 Oct 25, 2024
1793045
CSRF filter still problematic.
tonytw1 Oct 27, 2024
722fb5e
Marking TODO.
tonytw1 Nov 3, 2024
2dae723
Log /management/healthcheck as debug.
tonytw1 Nov 5, 2024
92f5f38
Private.
tonytw1 Nov 7, 2024
ca531c6
Update TODO.
tonytw1 Nov 7, 2024
85423a7
Disable Guardian email parsing of CSV file in bucket usage updates.
tonytw1 Nov 15, 2024
753b9c1
Refactor; QuotaStore in it's own file to make it more visible.
tonytw1 Nov 15, 2024
0e9f81d
usage/quotas needs a / prefix.
tonytw1 Nov 17, 2024
02ab5c6
Imports
tonytw1 Mar 20, 2026
ab5413c
We only need Environment credentials; may speed up first hit.
tonytw1 Nov 21, 2024
166b6df
Kanhuna CSP origin.full and origin.images are believed to be redundan…
tonytw1 Nov 24, 2024
237ec31
Resolve TODO. auth and kahuna are on the same hostname as everyother …
tonytw1 Nov 24, 2024
2f65116
Thrall exposes a /config end point so that reaping and hard delete co…
tonytw1 Nov 29, 2024
2826352
Hard delete can delete any image which is in the soft deleted state; …
tonytw1 Nov 29, 2024
dc7da2e
Thrall exposes a /config end point so that reaping and hard delete co…
tonytw1 Nov 29, 2024
b01e138
Logging.
tonytw1 Nov 29, 2024
aa1f294
Log delete folder number of files found.
tonytw1 Dec 3, 2024
333b348
Logging.
tonytw1 Dec 6, 2024
a60eec7
Disable set missing description to upload filename.
tonytw1 Dec 13, 2024
f592cd9
Revert "Disable set missing description to upload filename."
tonytw1 Mar 13, 2025
90f7bd1
Log quota store fetch.
tonytw1 Dec 14, 2024
06e089e
Log quota store fetch.
tonytw1 Dec 14, 2024
26ec593
Spike - no op quota + usage join.
tonytw1 Dec 14, 2024
449f501
Quota count on the right.
tonytw1 Dec 14, 2024
74a3fd9
Billing. Setup a new actor to capture usage events.
tonytw1 Jan 12, 2025
90b39a7
Move usage events to common lib for reuse.
tonytw1 Jan 6, 2025
7513065
Usage event for download original.
tonytw1 Jan 6, 2025
e130446
Usage event SQS sending.
tonytw1 Jan 6, 2025
6e032a6
Usage event includes date.
tonytw1 Jan 7, 2025
a1adca6
Emit api key used event on api auth.
tonytw1 Jan 11, 2025
0425799
Thrall config end point exposes maybeUploadLimitInBytes for UI.
tonytw1 Jan 12, 2025
837d68f
MB = 1024 * 1024.
tonytw1 Jan 12, 2025
cae6b8e
Remove source.secureURL from image response.
tonytw1 Jan 14, 2025
28df2e5
Set isFeedUpload field on uploaded and ingested images.
tonytw1 Jan 14, 2025
829d975
isFeedUpload moves to UploadInfo.
tonytw1 Jan 15, 2025
1b66465
Crops sign source.file to recreate secureUrl.
tonytw1 Feb 7, 2026
1e4d971
Correct image if for ingest events.
tonytw1 Jan 19, 2025
eeb976c
Clean up; auth instance call simplied.
tonytw1 Jan 20, 2025
226644c
Spike; machine auth has non user endpoint.
tonytw1 Jan 20, 2025
03dd367
Api keys have instances attribute; can be checked without an attached…
tonytw1 Jan 21, 2025
8601077
Log levels.
tonytw1 Feb 5, 2025
dd13f10
api key usage is recorded by key name.
tonytw1 Jan 23, 2025
182961a
Remove persistence.identifier config option which was historically se…
tonytw1 Jan 25, 2025
47f4282
Debug - log updateStatus
tonytw1 Jan 25, 2025
8328933
Debug - prepared to queue request fails.
tonytw1 Jan 25, 2025
2fde1db
Usage event for user auth.
tonytw1 Jan 23, 2025
3ab1b8c
Status.toString does not match the persisted format of the Dynamo tab…
tonytw1 Jan 25, 2025
b3f1a99
Debug - problem with uploadStatusTableWithCondition
tonytw1 Jan 25, 2025
395b5b6
Fix Prepare -> Queued conditional update by burning table and redefin…
tonytw1 Jan 25, 2025
f5da2ab
Ingest image presigned uploads need to be into the instance folders.
tonytw1 Jan 25, 2025
044837b
Disable publishChangedSyndicationRightsForPhotoshoot
tonytw1 Jan 29, 2025
67b7d19
Usage event for prepare upload.
tonytw1 Jan 30, 2025
0fa5598
Usage events; would like to capture user or api key inline.
tonytw1 Jan 30, 2025
cf495bb
Usage events; would like to capture user or api key inline.
tonytw1 Jan 30, 2025
473fe41
Usage events; api / user recorded for prepare upload.
tonytw1 Jan 30, 2025
2fea7ed
Usage events; api / user recorded for download image.
tonytw1 Jan 30, 2025
510af01
Move instances to common lib.
tonytw1 Feb 1, 2025
68b78c5
Move instances to common lib.
tonytw1 Feb 1, 2025
e051974
Thrall emits image delete events.
tonytw1 Feb 2, 2025
98899fd
Preview reapable query by removing the time window when selected as i…
tonytw1 Feb 3, 2025
9a7738b
ReapableEligibility checks for isFeedUpload.
tonytw1 Feb 7, 2025
0bc07cd
Logging.
tonytw1 Feb 7, 2025
321a03c
Log image upload duration.
tonytw1 Feb 28, 2026
a301d07
ReapableEligibility checks for isFeedUpload.
tonytw1 Feb 7, 2025
006abca
ReapableEligibility checks for isFeedUpload.
tonytw1 Feb 7, 2025
a568502
Log mime type.
tonytw1 Feb 15, 2025
5111862
Feed ingest file uploader is last folder in path.
tonytw1 Feb 26, 2025
bf23536
Feed ingest file uploader is folder after feeds.
tonytw1 Feb 26, 2025
ed64e9f
Feed ingest file uploader is folder after feeds.
tonytw1 Feb 26, 2025
ffea2da
Generalise text for Chargable usage rights description.
tonytw1 Apr 12, 2025
5d013cb
Log S3 bulk deletes.
tonytw1 Apr 13, 2025
006462a
Log total reapable images.
tonytw1 Apr 14, 2025
c14f1f7
private
tonytw1 Apr 19, 2025
a958c46
Thrall kinesis appName is explicitly set rather than defaulting to th…
tonytw1 Apr 20, 2025
7a3a8a6
Thrall kinesis appName is explicitly set rather than defaulting to th…
tonytw1 Apr 20, 2025
1c2acc9
Log stream name for Kinesis put.
tonytw1 Apr 20, 2025
24e1721
Log initial offset.
tonytw1 Apr 20, 2025
03343b9
Mulitple KCL streams cannot share the same checkpointing table!
tonytw1 Apr 20, 2025
f04585a
Log levels; reaper bucket not configured.
tonytw1 Apr 28, 2025
d13e5f9
Increase polling interval for unused /notification to 10 minutes.
tonytw1 Apr 28, 2025
fb08c15
Generalise text for Chargable usage rights description.
tonytw1 Apr 12, 2025
59374cc
Generalise owned by. Screen grab description uses generalised text.
tonytw1 Apr 13, 2025
724c02d
Generalise owned by. Remove org-owned: prefix on image thumbnail mous…
tonytw1 Apr 12, 2025
ac7285c
Generalise owned filter names.
tonytw1 Apr 12, 2025
69a2a99
Generalise owned by - is suggestion names.
tonytw1 Feb 5, 2026
fb95823
Generalise the sensitive content explainer.
tonytw1 Mar 1, 2026
5b252ba
Instances is more testable if injected rather than mixed in.
tonytw1 Sep 21, 2025
a7289c6
Setting up for instance aware migration. Push down the hard-coded dum…
tonytw1 Aug 17, 2025
cf5d7eb
Migration source and status have list of instances.
tonytw1 Aug 17, 2025
9f5dcf7
[multi-tenant] Refactor; group getMyInstances into Instances trait.
tonytw1 Nov 2, 2025
732d416
[multi-tenant] Do not quietly recover from instances call failures. N…
tonytw1 Nov 2, 2025
c08580f
[multi-tenant] Download export link is instance aware
tonytw1 Feb 8, 2026
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
6 changes: 4 additions & 2 deletions auth/app/auth/AuthConfig.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package auth

import com.gu.mediaservice.lib.config.{CommonConfig, GridConfigResources}
import com.gu.mediaservice.model.Instance

class AuthConfig(resources: GridConfigResources) extends CommonConfig(resources) {
val rootUri: String = services.authBaseUri
val mediaApiUri: String = services.apiBaseUri
val rootUri: Instance => String = services.authBaseUri
val rootInstanceUri: Instance => String = services.authBaseInstanceUri
val mediaApiUri: Instance => String = services.apiBaseUri
}
21 changes: 13 additions & 8 deletions auth/app/auth/AuthController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import com.gu.mediaservice.lib.auth.Authentication.{InnerServicePrincipal, Machi
import com.gu.mediaservice.lib.auth.Permissions.{DeleteImage, ShowPaid, UploadImages}
import com.gu.mediaservice.lib.auth.provider.AuthenticationProviders
import com.gu.mediaservice.lib.auth.{Authentication, Authorisation, Internal}
import com.gu.mediaservice.lib.config.InstanceForRequest
import com.gu.mediaservice.lib.guardian.auth.PandaAuthenticationProvider
import com.gu.mediaservice.model.Instance
import play.api.libs.json.Json
import play.api.mvc.{BaseController, ControllerComponents, Result}
import play.api.mvc.{AnyContent, BaseController, ControllerComponents, Request, Result}

import java.net.URI
import java.util.Date
Expand All @@ -19,15 +21,15 @@ class AuthController(auth: Authentication, providers: AuthenticationProviders, v
override val controllerComponents: ControllerComponents,
authorisation: Authorisation)(implicit ec: ExecutionContext)
extends BaseController
with ArgoHelpers {
with ArgoHelpers with InstanceForRequest {

val indexResponse = {
def indexResponse()(implicit instance: Instance) = {
val indexData = Map("description" -> "This is the Auth API")
val indexLinks = List(
Link("root", config.mediaApiUri),
Link("login", config.services.loginUriTemplate),
Link("ui:logout", s"${config.rootUri}/logout"),
Link("session", s"${config.rootUri}/session")
Link("root", config.mediaApiUri(instance)),
Link("login", config.services.loginUriTemplate(instance)),
Link("ui:logout", s"${config.rootUri(instance)}/logout"),
Link("session", s"${config.rootInstanceUri(instance)}/session")
)
respond(indexData, indexLinks)
}
Expand All @@ -42,7 +44,10 @@ class AuthController(auth: Authentication, providers: AuthenticationProviders, v
}
}

def index = auth { indexResponse }
def index = auth { request =>
implicit val instance: Instance = instanceOf(request)
indexResponse()
}

def session = auth { request =>
val showPaid = authorisation.hasPermissionTo(ShowPaid)(request.user)
Expand Down
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ lazy val commonLib = project("common-lib").settings(
"com.amazonaws" % "aws-java-sdk-s3" % awsSdkVersion,
"com.amazonaws" % "aws-java-sdk-ec2" % awsSdkVersion,
"com.amazonaws" % "aws-java-sdk-sqs" % awsSdkVersion,
"software.amazon.awssdk" % "sqs" % awsSdkV2Version,
"com.amazonaws" % "aws-java-sdk-sns" % awsSdkVersion,
"com.amazonaws" % "aws-java-sdk-sts" % awsSdkVersion,
"com.amazonaws" % "aws-java-sdk-kinesis" % awsSdkVersion,
Expand Down Expand Up @@ -183,7 +184,8 @@ lazy val thrall = playProject("thrall", 9002)
"software.amazon.awssdk" % "dynamodb" % awsSdkV2Version,
"com.gu" %% "kcl-pekko-stream" % "0.1.0",
"org.testcontainers" % "testcontainers-elasticsearch" % "2.0.2" % Test,
"com.google.protobuf" % "protobuf-java" % "3.19.6"
"com.google.protobuf" % "protobuf-java" % "3.19.6",
"software.amazon.awssdk" % "sqs" % awsSdkV2Version
),
// amazon-kinesis-client 2.6.0 brings in a critically vulnerable version of apache avro,
// but we cannot upgrade amazon-kinesis-client further without performing the v2->v3 upgrade https://docs.aws.amazon.com/streams/latest/dev/kcl-migration-from-2-3.html
Expand Down
2 changes: 1 addition & 1 deletion collections/app/CollectionsComponents.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CollectionsComponents(context: Context) extends GridComponents(context, ne
val notifications = new Notifications(config)

val collections = new CollectionsController(auth, config, collectionsStore, controllerComponents)
val imageCollections = new ImageCollectionsController(auth, config, notifications, imageCollectionsStore, controllerComponents)
val imageCollections = new ImageCollectionsController(auth, notifications, imageCollectionsStore, controllerComponents)


override val router = new Routes(httpErrorHandler, collections, imageCollections, management)
Expand Down
71 changes: 39 additions & 32 deletions collections/app/controllers/CollectionsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import com.gu.mediaservice.lib.argo.model.{EmbeddedEntity, Link}
import com.gu.mediaservice.lib.auth.Authentication
import com.gu.mediaservice.lib.auth.Authentication.getIdentity
import com.gu.mediaservice.lib.collections.CollectionsManager
import com.gu.mediaservice.model.{ActionData, Collection}
import com.gu.mediaservice.lib.config.InstanceForRequest
import com.gu.mediaservice.model.{ActionData, Collection, Instance}
import lib.CollectionsConfig
import model.Node
import org.joda.time.DateTime
import play.api.libs.functional.syntax._
import play.api.libs.json._
import play.api.mvc.{BaseController, ControllerComponents}
import play.api.mvc.{BaseController, ControllerComponents, Request}
import store.{CollectionsStore, CollectionsStoreError}
import com.gu.mediaservice.lib.net.{URI => UriOps}
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
import com.gu.mediaservice.lib.net.{URI => UriOps}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
Expand All @@ -31,31 +33,31 @@ object AppIndex {
}

class CollectionsController(authenticated: Authentication, config: CollectionsConfig, store: CollectionsStore,
val controllerComponents: ControllerComponents) extends BaseController with ArgoHelpers {
val controllerComponents: ControllerComponents) extends BaseController with ArgoHelpers with InstanceForRequest {

import CollectionsManager.{getCssColour, isValidPathBit, pathToUri, uriToPath}
// Stupid name clash between Argo and Play
import com.gu.mediaservice.lib.argo.model.{Action => ArgoAction}

def uri(u: String) = URI.create(u)
val collectionUri = uri(s"${config.rootUri}/collections")
def collectionUri(p: List[String] = Nil) = {
private def uri(u: String) = URI.create(u)
private def collectionUri()(implicit instance: Instance) = uri(s"${config.rootUri(instance)}/collections")
private def collectionUri(p: List[String] = Nil)(implicit instance: Instance) = {
val path = if(p.nonEmpty) s"/${pathToUri(p)}" else ""
uri(s"${config.rootUri}/collections$path")
uri(s"${config.rootUri(instance)}/collections$path")
}

val appIndex = AppIndex("media-collections", "The one stop shop for collections")
val indexLinks = List(Link("collections", collectionUri.toString))
private val appIndex = AppIndex("media-collections", "The one stop shop for collections")
private def indexLinks()(implicit instance: Instance) = List(Link("collections", collectionUri().toString))

def getNodeAction(n: Node[Collection]): Option[Link] = Some(Link("collection", collectionUri(n.fullPath).toString))
def addChildAction(pathId: List[String] = Nil): Option[ArgoAction] = Some(ArgoAction("add-child", collectionUri(pathId), "POST"))
def addChildAction(n: Node[Collection]): Option[ArgoAction] = addChildAction(n.fullPath)
def removeNodeAction(n: Node[Collection]): Option[ArgoAction] = if (n.children.nonEmpty) None else Some(
private def getNodeAction(n: Node[Collection])(implicit instance: Instance): Option[Link] = Some(Link("collection", collectionUri(n.fullPath).toString))
private def addChildAction(pathId: List[String] = Nil)(implicit instance: Instance): Option[ArgoAction] = Some(ArgoAction("add-child", collectionUri(pathId), "POST"))
private def addChildAction(n: Node[Collection])(implicit instance: Instance): Option[ArgoAction] = addChildAction(n.fullPath)
private def removeNodeAction(n: Node[Collection])(implicit instance: Instance): Option[ArgoAction] = if (n.children.nonEmpty) None else Some(
ArgoAction("remove", collectionUri(n.fullPath), "DELETE")
)

def index = authenticated { req =>
respond(appIndex, links = indexLinks)
respond(appIndex, links = indexLinks()(instanceOf(req)))
}

def collectionNotFound(path: String) =
Expand All @@ -70,15 +72,16 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
def storeError(message: String) =
respondError(InternalServerError, "collection-store-error", message)

def getActions(n: Node[Collection]): List[ArgoAction] = {
def getActions(n: Node[Collection])(implicit instance: Instance): List[ArgoAction] = {
List(addChildAction(n), removeNodeAction(n)).flatten
}

def getLinks(n: Node[Collection]): List[Link] = {
private def getLinks(n: Node[Collection])(implicit instance: Instance): List[Link] = {
List(getNodeAction(n)).flatten
}

def correctedCollections = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
store.getAll flatMap { collections =>
val tree = Node.fromList[Collection](
collections,
Expand All @@ -100,14 +103,15 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}
}

def allCollections = store.getAll.map { collections =>
def allCollections()(implicit instance: Instance)= store.getAll.map { collections =>
Node.fromList[Collection](
collections,
(collection) => collection.path,
(collection) => collection.description)
}

def getCollection(collectionPathId: String) = authenticated.async {
def getCollection(collectionPathId: String) = authenticated.async { request =>
implicit val instance: Instance = instanceOf(request)
store.get(uriToPath(collectionPathId)).map {
case Some(collection) =>
val node = Node(collection.path.last, Nil, collection.path, collection.path, Some(collection))
Expand All @@ -120,7 +124,18 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}

def getCollections = authenticated.async { req =>
allCollections.map { tree =>
implicit val instance: Instance = instanceOf(req)
implicit def asArgo: Writes[Node[Collection]] = (
(__ \ "basename").write[String] ~
(__ \ "children").lazyWrite[CollectionsEntity](Writes[CollectionsEntity]
// This is so we don't have to rewrite the Write[Seq[T]]
(seq => Json.toJson(seq))).contramap(collectionsEntity(_: List[Node[Collection]])) ~
(__ \ "fullPath").write[List[String]] ~
(__ \ "data").writeNullable[Collection] ~
(__ \ "cssColour").writeNullable[String]
)(node => (node.basename, node.children, node.fullPath, node.data, getCssColour(node.fullPath)))

allCollections().map { tree =>
respond(
Json.toJson(tree)(asArgo),
actions = List(addChildAction()).flatten
Expand All @@ -134,6 +149,7 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
def addChildToRoot = addChildTo(None)
def addChildToCollection(collectionPathId: String) = addChildTo(Some(collectionPathId))
def addChildTo(collectionPathId: Option[String]) = authenticated.async(parse.json) { req =>
implicit val instance: Instance = instanceOf(req)
(req.body \ "data").asOpt[String] map { child =>
if (isValidPathBit(child)) {
val path = collectionPathId.map(uriToPath).getOrElse(Nil) :+ child
Expand All @@ -153,8 +169,8 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}

type MaybeTree = Option[Node[Collection]]
def hasChildren(path: List[String]): Future[Boolean] =
allCollections.map { tree =>
private def hasChildren(path: List[String])(implicit instance: Instance) =
allCollections().map { tree =>

// Traverse the tree using the path
val maybeTree = path
Expand All @@ -168,6 +184,7 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}

def removeCollection(collectionPath: String) = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
val path = CollectionsManager.uriToPath(UriOps.encodePlus(collectionPath))

hasChildren(path).flatMap { noRemove =>
Expand Down Expand Up @@ -195,18 +212,8 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
)(node => (node.basename, node.children, node.fullPath, node.data))

type CollectionsEntity = Seq[EmbeddedEntity[Node[Collection]]]
implicit def asArgo: Writes[Node[Collection]] = (
(__ \ "basename").write[String] ~
(__ \ "children").lazyWrite[CollectionsEntity](Writes[CollectionsEntity]
// This is so we don't have to rewrite the Write[Seq[T]]
(seq => Json.toJson(seq))).contramap(collectionsEntity) ~
(__ \ "fullPath").write[List[String]] ~
(__ \ "data").writeNullable[Collection] ~
(__ \ "cssColour").writeNullable[String]
)(node => (node.basename, node.children, node.fullPath, node.data, getCssColour(node.fullPath)))


def collectionsEntity(nodes: List[Node[Collection]]): CollectionsEntity = {
private def collectionsEntity(nodes: List[Node[Collection]])(implicit instance: Instance): CollectionsEntity = {
nodes.map(n => EmbeddedEntity(collectionUri(n.fullPath), Some(n), links = getLinks(n), actions = getActions(n)))
}

Expand Down
17 changes: 10 additions & 7 deletions collections/app/controllers/ImageCollectionsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,28 @@ import com.gu.mediaservice.lib.auth.Authentication
import com.gu.mediaservice.lib.auth.Authentication.getIdentity
import com.gu.mediaservice.lib.aws.{NoItemFound, UpdateMessage}
import com.gu.mediaservice.lib.collections.CollectionsManager
import com.gu.mediaservice.lib.config.InstanceForRequest
import com.gu.mediaservice.lib.net.{URI => UriOps}
import com.gu.mediaservice.model.{ActionData, Collection}
import com.gu.mediaservice.model.{ActionData, Collection, Instance}
import com.gu.mediaservice.syntax.MessageSubjects
import lib.{CollectionsConfig, Notifications}
import lib.Notifications
import org.joda.time.DateTime
import play.api.libs.json.Json
import play.api.mvc.{BaseController, ControllerComponents}
import store.ImageCollectionsStore

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future


class ImageCollectionsController(authenticated: Authentication, config: CollectionsConfig, notifications: Notifications,
class ImageCollectionsController(authenticated: Authentication, notifications: Notifications,
imageCollectionsStore: ImageCollectionsStore,
override val controllerComponents: ControllerComponents)
extends BaseController with MessageSubjects with ArgoHelpers {
extends BaseController with MessageSubjects with ArgoHelpers with InstanceForRequest {

import CollectionsManager.onlyLatest

def getCollections(id: String) = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
imageCollectionsStore.get(id).map { collections =>
respond(onlyLatest(collections))
} recover {
Expand All @@ -34,6 +35,7 @@ class ImageCollectionsController(authenticated: Authentication, config: Collecti
}

def addCollection(id: String) = authenticated.async(parse.json) { req =>
implicit val instance: Instance = instanceOf(req)
(req.body \ "data").asOpt[List[String]].map { path =>
val collection = Collection.build(path, ActionData(getIdentity(req.user), DateTime.now()))
imageCollectionsStore.add(id, collection)
Expand All @@ -44,6 +46,7 @@ class ImageCollectionsController(authenticated: Authentication, config: Collecti


def removeCollection(id: String, collectionString: String) = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
val path = CollectionsManager.uriToPath(UriOps.encodePlus(collectionString))
// We do a get to be able to find the index of the current collection, then remove it.
// Given that we're using Dynamo Lists this seemed like a decent way to do it.
Expand All @@ -63,9 +66,9 @@ class ImageCollectionsController(authenticated: Authentication, config: Collecti
}
}

def publish(id: String)(collections: List[Collection]): List[Collection] = {
def publish(id: String)(collections: List[Collection])(implicit instance: Instance): List[Collection] = {
val onlyLatestCollections = onlyLatest(collections)
val updateMessage = UpdateMessage(subject = SetImageCollections, id = Some(id), collections = Some(onlyLatestCollections))
val updateMessage = UpdateMessage(subject = SetImageCollections, id = Some(id), collections = Some(onlyLatestCollections), instance = instance)
notifications.publish(updateMessage)
onlyLatestCollections
}
Expand Down
3 changes: 2 additions & 1 deletion collections/app/lib/CollectionsConfig.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package lib

import com.gu.mediaservice.lib.config.{CommonConfig, GridConfigResources}
import com.gu.mediaservice.model.Instance


class CollectionsConfig(resources: GridConfigResources) extends CommonConfig(resources) {
val collectionsTable = string("dynamo.table.collections")
val imageCollectionsTable = string("dynamo.table.imageCollections")

val rootUri = services.collectionsBaseUri
val rootUri: Instance => String = services.collectionsBaseUri
}
25 changes: 12 additions & 13 deletions collections/app/store/CollectionsStore.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package store

import cats.implicits._
import com.gu.mediaservice.lib.collections.CollectionsManager
import com.gu.mediaservice.model.{ActionData, Collection}
import com.gu.mediaservice.model.{ActionData, Collection, Instance}
import org.joda.time.DateTime
import org.scanamo.generic.auto.genericDerivedFormat
import org.scanamo.{DynamoFormat, ScanamoAsync, Table}
import org.scanamo.syntax._
import org.scanamo.{DynamoFormat, ScanamoAsync, Table}
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import cats.implicits._
import org.scanamo.generic.semiauto.FieldName

case class Record(id: String, collection: Collection)
case class Record(id: String, collection: Collection, instance: String)

class CollectionsStore(val tableName: String, client: DynamoDbAsyncClient) extends DynamoHelpers {
import org.scanamo.generic.semiauto._
Expand All @@ -25,24 +24,24 @@ class CollectionsStore(val tableName: String, client: DynamoDbAsyncClient) exten

private lazy val collectionsTable = Table[Record](tableName)

def getAll: Future[List[Collection]] = {
ScanamoAsync(client).exec(collectionsTable.scan()).map(_.sequence).flatMap(res =>
def getAll(implicit instance: Instance): Future[List[Collection]] = {
ScanamoAsync(client).exec(collectionsTable.query("instance" === instance.id)).map(_.sequence).flatMap(res =>
handleResponse(res)(records => records.map(_.collection))
)
}

def add(collection: Collection): Future[Collection] = {
def add(collection: Collection)(implicit instance: Instance): Future[Collection] = {
ScanamoAsync(client).exec(
collectionsTable.update(
"id" === collection.pathId,
"id" === collection.pathId and "instance" === instance.id,
set("collection", collection)
)
).flatMap(res => handleResponse(res)(record => record.collection))
}

def get(collectionPath: List[String]): Future[Option[Collection]] = {
def get(collectionPath: List[String])(implicit instance: Instance): Future[Option[Collection]] = {
val path = CollectionsManager.pathToPathId(collectionPath)
ScanamoAsync(client).exec(collectionsTable.get("id" === path)).flatMap(maybeEither =>
ScanamoAsync(client).exec(collectionsTable.get("id" === path and "instance" === instance.id)).flatMap(maybeEither =>
maybeEither.fold[Future[Option[Collection]]](
Future.successful(None)
)(res =>
Expand All @@ -51,9 +50,9 @@ class CollectionsStore(val tableName: String, client: DynamoDbAsyncClient) exten
)
}

def remove(collectionPath: List[String]): Future[Unit] = {
def remove(collectionPath: List[String])(implicit instance: Instance): Future[Unit] = {
val path = CollectionsManager.pathToPathId(collectionPath)
ScanamoAsync(client).exec(collectionsTable.delete("id" === path))
ScanamoAsync(client).exec(collectionsTable.delete("id" === path and "instance" === instance.id))
}
}

Expand Down
Loading
Loading