@@ -42,12 +42,12 @@ func NewMinter(bucket string, impersonateSA string) *Minter {
4242}
4343
4444// MintDownscopedToken creates a downscoped token for volume operations.
45- // The token is scoped to the volumes bucket with minimal permissions:
46- // - objectViewer: list + get (for litestream restore)
47- // - objectCreator: create (for litestream replicate and juicefs write)
45+ // The token is scoped to the specific volume prefix with minimal permissions:
46+ // - objectAdmin: list + get + create (restricted to volumeID/ and volumeID-meta/ prefixes)
4847//
49- // Note: No delete permission. No volume isolation via CAB (litestream list
50- // operations fail with prefix conditions). Time-limited to 1 hour.
48+ // Uses CAB availabilityCondition with:
49+ // - resource.name.startsWith() for GET/PUT operations
50+ // - api.getAttribute('storage.googleapis.com/objectListPrefix') for LIST operations
5151func (m * Minter ) MintDownscopedToken (ctx context.Context , volumeID string ) (* Token , error ) {
5252 // Step 1: Get base token (either via impersonation or directly from metadata)
5353 baseToken , err := m .getBaseToken (ctx )
@@ -56,21 +56,30 @@ func (m *Minter) MintDownscopedToken(ctx context.Context, volumeID string) (*Tok
5656 }
5757
5858 // Step 2: Create credential access boundary with minimal permissions
59- // Using viewer + creator instead of objectAdmin to exclude delete
59+ // Using objectAdmin with CEL condition to restrict to volume prefix
6060 bucketResource := fmt .Sprintf ("//storage.googleapis.com/projects/_/buckets/%s" , m .bucket )
6161
62+ // Build CEL condition for volume isolation
63+ // - resource.name.startsWith() for GET/PUT operations
64+ // - api.getAttribute('storage.googleapis.com/objectListPrefix') for LIST operations
65+ prefixCondition := fmt .Sprintf (
66+ "resource.name.startsWith('projects/_/buckets/%s/objects/%s/') || " +
67+ "resource.name.startsWith('projects/_/buckets/%s/objects/%s-meta/') || " +
68+ "api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('%s/') || " +
69+ "api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('%s-meta/')" ,
70+ m .bucket , volumeID , m .bucket , volumeID , volumeID , volumeID ,
71+ )
72+
6273 cab := CredentialAccessBoundary {
6374 AccessBoundary : AccessBoundary {
6475 AccessBoundaryRules : []AccessBoundaryRule {
6576 {
66- // objectViewer: storage.objects.list + storage.objects.get
67- AvailablePermissions : []string {"inRole:roles/storage.objectViewer" },
68- AvailableResource : bucketResource ,
69- },
70- {
71- // objectCreator: storage.objects.create
72- AvailablePermissions : []string {"inRole:roles/storage.objectCreator" },
77+ AvailablePermissions : []string {"inRole:roles/storage.objectAdmin" },
7378 AvailableResource : bucketResource ,
79+ AvailabilityCondition : & AvailabilityCondition {
80+ Title : "Volume isolation" ,
81+ Expression : prefixCondition ,
82+ },
7483 },
7584 },
7685 },
@@ -250,5 +259,6 @@ type AccessBoundaryRule struct {
250259
251260// AvailabilityCondition is a CEL expression that further restricts access.
252261type AvailabilityCondition struct {
262+ Title string `json:"title,omitempty"`
253263 Expression string `json:"expression"`
254264}
0 commit comments