Skip to content
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
2 changes: 2 additions & 0 deletions docs/configuration/advanced/application-yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,15 @@ floci-gcp:
enabled: false
iam:
enabled: false
authorization-mode: disabled # disabled | enforce
```

Via environment variable:

```bash
FLOCI_GCP_SERVICES_DATASTORE_ENABLED=false
FLOCI_GCP_SERVICES_IAM_ENABLED=false
FLOCI_GCP_SERVICES_IAM_AUTHORIZATION_MODE=disabled
```

## Logging
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Each service can be toggled independently. All are enabled by default.
| `FLOCI_GCP_SERVICES_DATASTORE_ENABLED` | `true` | Datastore |
| `FLOCI_GCP_SERVICES_SECRETMANAGER_ENABLED` | `true` | Secret Manager |
| `FLOCI_GCP_SERVICES_IAM_ENABLED` | `true` | IAM |
| `FLOCI_GCP_SERVICES_IAM_AUTHORIZATION_MODE` | `disabled` | IAM allow-policy evaluation mode. `disabled` preserves no-auth behavior; `enforce` is reserved for the forthcoming supported evaluation surface |
| `FLOCI_GCP_SERVICES_IAMCREDENTIALS_ENABLED` | `true` | IAM Service Account Credentials (`generateAccessToken`) |
| `FLOCI_GCP_SERVICES_STS_ENABLED` | `true` | Security Token Service (STS) |
| `FLOCI_GCP_SERVICES_LOGGING_ENABLED` | `true` | Cloud Logging |
Expand Down
6 changes: 6 additions & 0 deletions docs/services/iam.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ floci-gcp emulates Google Cloud IAM over REST JSON using the real GCP IAM API.
| Variable | Default | Description |
|---|---|---|
| `FLOCI_GCP_SERVICES_IAM_ENABLED` | `true` | Enable/disable IAM |
| `FLOCI_GCP_SERVICES_IAM_AUTHORIZATION_MODE` | `disabled` | IAM allow-policy evaluation mode. `disabled` preserves current no-auth behavior; `enforce` is reserved for the forthcoming supported evaluation surface |

`authorization-mode` defaults to `disabled`. IAM policy storage and policy-shaped
responses remain available in that mode, but they do not restrict requests. The
setting is introduced ahead of the evaluator; this slice does not enforce
policies even when it is configured as `enforce`.

## Quick Start

Expand Down
26 changes: 26 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,24 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectnessie.cel</groupId>
<artifactId>cel-bom</artifactId>
<version>0.8.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- CEL 0.8.0 generated protobuf classes require a 4.35.1 runtime. -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.35.1</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>4.35.1</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down Expand Up @@ -256,6 +274,14 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectnessie.cel</groupId>
<artifactId>cel-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectnessie.cel</groupId>
<artifactId>cel-generated-pb</artifactId>
</dependency>

<!-- Docker client -->
<dependency>
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/io/floci/gcp/config/EmulatorConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ interface DatastoreServiceConfig {
interface IamServiceConfig {
@WithDefault("true")
boolean enabled();

@WithDefault("disabled")
IamAuthorizationMode authorizationMode();
}

enum IamAuthorizationMode {
DISABLED,
ENFORCE
}

interface IamCredentialsServiceConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ public void updateBucket(UpdateBucketRequest request, StreamObserver<Bucket> obs
if (request.getUpdateMask().getPathsCount() == 0) {
throw GcpException.invalidArgument("update_mask is required");
}
return GcsGrpcMapper.toProto(service.updateBucket(bucketId,
return GcsGrpcMapper.toProto(service.updateBucketWithResolvedFields(bucketId,
GcsGrpcMapper.bucketUpdateFields(
request.getBucket(), request.getUpdateMask().getPathsList())));
current, request.getBucket(), request.getUpdateMask().getPathsList())));
});
}

Expand Down
144 changes: 142 additions & 2 deletions src/main/java/io/floci/gcp/services/gcs/GcsGrpcMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ static Bucket toProto(GcsBucket stored) {
value.setVersioning(Bucket.Versioning.newBuilder()
.setEnabled(Boolean.TRUE.equals(stored.getVersioning().get("enabled"))));
}
if (stored.getIamConfiguration() != null) {
Bucket.IamConfig.Builder iamConfig = Bucket.IamConfig.newBuilder();
if (stored.getIamConfiguration().get("uniformBucketLevelAccess") instanceof Map<?, ?> access) {
Bucket.IamConfig.UniformBucketLevelAccess.Builder uniformAccess =
Bucket.IamConfig.UniformBucketLevelAccess.newBuilder()
.setEnabled(Boolean.TRUE.equals(access.get("enabled")));
if (access.get("lockedTime") instanceof String lockedTime) {
timestamp(lockedTime).ifPresent(uniformAccess::setLockTime);
}
iamConfig.setUniformBucketLevelAccess(uniformAccess);
}
if (stored.getIamConfiguration().get("publicAccessPrevention") instanceof String prevention) {
iamConfig.setPublicAccessPrevention(prevention);
}
if (iamConfig.hasUniformBucketLevelAccess() || !iamConfig.getPublicAccessPrevention().isBlank()) {
value.setIamConfig(iamConfig);
}
}
return value.build();
}

Expand All @@ -83,22 +101,37 @@ static Map<String, java.lang.Object> bucketCreateFields(Bucket bucket) {
if (bucket.hasVersioning()) {
body.put("versioning", Map.of("enabled", bucket.getVersioning().getEnabled()));
}
iamConfiguration(bucket).ifPresent(iamConfiguration -> body.put("iamConfiguration", iamConfiguration));
body.put("defaultEventBasedHold", bucket.getDefaultEventBasedHold());
return body;
}

static Map<String, java.lang.Object> bucketUpdateFields(Bucket bucket,
static Map<String, java.lang.Object> bucketUpdateFields(GcsBucket current, Bucket bucket,
java.util.List<String> paths) {
Map<String, java.lang.Object> all = bucketCreateFields(bucket);
if (paths.contains("*")) {
Map<String, java.lang.Object> all = bucketCreateFields(bucket);
all.putIfAbsent("iamConfiguration", Map.of());
return all;
}
Map<String, java.lang.Object> patch = new LinkedHashMap<>();
boolean mergeIamConfigurationMessage = false;
boolean mergeUniformBucketLevelAccessMessage = false;
boolean updateUniformBucketLevelAccessEnabled = false;
boolean updateUniformBucketLevelAccessLockTime = false;
boolean updatePublicAccessPrevention = false;
for (String path : paths) {
switch (path) {
case "labels" -> patch.put("labels", new LinkedHashMap<>(bucket.getLabelsMap()));
case "versioning", "versioning.enabled" ->
patch.put("versioning", Map.of("enabled", bucket.getVersioning().getEnabled()));
case "iam_config" -> mergeIamConfigurationMessage = true;
case "iam_config.uniform_bucket_level_access" ->
mergeUniformBucketLevelAccessMessage = true;
case "iam_config.uniform_bucket_level_access.enabled" ->
updateUniformBucketLevelAccessEnabled = true;
case "iam_config.uniform_bucket_level_access.lock_time" ->
updateUniformBucketLevelAccessLockTime = true;
case "iam_config.public_access_prevention" -> updatePublicAccessPrevention = true;
case "storage_class" -> patch.put("storageClass", bucket.getStorageClass());
case "default_event_based_hold" ->
patch.put("defaultEventBasedHold", bucket.getDefaultEventBasedHold());
Expand All @@ -111,9 +144,112 @@ static Map<String, java.lang.Object> bucketUpdateFields(Bucket bucket,
}
}
}
if (mergeIamConfigurationMessage || mergeUniformBucketLevelAccessMessage
|| updateUniformBucketLevelAccessEnabled || updateUniformBucketLevelAccessLockTime
|| updatePublicAccessPrevention) {
patch.put("iamConfiguration", mergeIamConfiguration(
current.getIamConfiguration(), bucket,
mergeIamConfigurationMessage, mergeUniformBucketLevelAccessMessage,
updateUniformBucketLevelAccessEnabled, updateUniformBucketLevelAccessLockTime,
updatePublicAccessPrevention));
}
return patch;
}

private static Map<String, java.lang.Object> mergeIamConfiguration(
Map<String, java.lang.Object> current, Bucket bucket,
boolean mergeIamConfigurationMessage, boolean mergeUniformBucketLevelAccessMessage,
boolean updateUniformBucketLevelAccessEnabled,
boolean updateUniformBucketLevelAccessLockTime,
boolean updatePublicAccessPrevention) {
Map<String, java.lang.Object> merged = current == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(current);
Bucket.IamConfig requested = bucket.getIamConfig();
// Protobuf FieldMask update semantics merge a selected message into the
// stored message, preserving omitted siblings. A leaf mask can clear one field.
if (mergeIamConfigurationMessage) {
if (bucket.hasIamConfig()) {
if (requested.hasUniformBucketLevelAccess()) {
merged.put("uniformBucketLevelAccess", mergeUniformBucketLevelAccess(
merged.get("uniformBucketLevelAccess"),
requested.getUniformBucketLevelAccess()));
}
if (!requested.getPublicAccessPrevention().isBlank()) {
merged.put("publicAccessPrevention", requested.getPublicAccessPrevention());
}
}
return merged;
}

if (mergeUniformBucketLevelAccessMessage) {
if (requested.hasUniformBucketLevelAccess()) {
merged.put("uniformBucketLevelAccess", mergeUniformBucketLevelAccess(
merged.get("uniformBucketLevelAccess"),
requested.getUniformBucketLevelAccess()));
}
} else if (updateUniformBucketLevelAccessEnabled || updateUniformBucketLevelAccessLockTime) {
Map<String, java.lang.Object> uniformAccess = mutableMap(
merged.get("uniformBucketLevelAccess"));
if (updateUniformBucketLevelAccessEnabled) {
uniformAccess.put("enabled", requested.getUniformBucketLevelAccess().getEnabled());
}
if (updateUniformBucketLevelAccessLockTime) {
if (requested.getUniformBucketLevelAccess().hasLockTime()) {
uniformAccess.put("lockedTime",
instant(requested.getUniformBucketLevelAccess().getLockTime()));
} else {
uniformAccess.remove("lockedTime");
}
}
merged.put("uniformBucketLevelAccess", uniformAccess);
}

if (updatePublicAccessPrevention) {
if (requested.getPublicAccessPrevention().isBlank()) {
merged.remove("publicAccessPrevention");
} else {
merged.put("publicAccessPrevention", requested.getPublicAccessPrevention());
}
}
return merged;
}

private static Map<String, java.lang.Object> mergeUniformBucketLevelAccess(
java.lang.Object current, Bucket.IamConfig.UniformBucketLevelAccess requested) {
Map<String, java.lang.Object> merged = mutableMap(current);
// Official clients can select the parent IAM field while sending only this
// submessage. Its presence makes the plain proto3 false value intentional.
merged.put("enabled", requested.getEnabled());
return merged;
}

private static Map<String, java.lang.Object> mutableMap(java.lang.Object value) {
Map<String, java.lang.Object> copy = new LinkedHashMap<>();
if (value instanceof Map<?, ?> map) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
copy.put(String.valueOf(entry.getKey()), entry.getValue());
}
}
return copy;
}

private static java.util.Optional<Map<String, java.lang.Object>> iamConfiguration(Bucket bucket) {
if (!bucket.hasIamConfig()) {
return java.util.Optional.empty();
}
Map<String, java.lang.Object> iamConfiguration = new LinkedHashMap<>();
if (bucket.getIamConfig().hasUniformBucketLevelAccess()) {
Map<String, java.lang.Object> uniformAccess = new LinkedHashMap<>();
uniformAccess.put("enabled", bucket.getIamConfig().getUniformBucketLevelAccess().getEnabled());
iamConfiguration.put("uniformBucketLevelAccess", uniformAccess);
}
if (!bucket.getIamConfig().getPublicAccessPrevention().isBlank()) {
iamConfiguration.put("publicAccessPrevention", bucket.getIamConfig().getPublicAccessPrevention());
}
return iamConfiguration.isEmpty() ? java.util.Optional.empty() : java.util.Optional.of(iamConfiguration);
}

static com.google.storage.v2.Object toProto(GcsObjectMeta stored) {
com.google.storage.v2.Object.Builder value = com.google.storage.v2.Object.newBuilder()
.setName(stored.getName())
Expand Down Expand Up @@ -216,6 +352,10 @@ static java.util.Optional<Timestamp> timestamp(String value) {
.setSeconds(instant.getEpochSecond()).setNanos(instant.getNano()).build());
}

private static String instant(Timestamp timestamp) {
return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()).toString();
}

private static long parseLong(String value) {
return value == null || value.isBlank() ? 0 : Long.parseLong(value);
}
Expand Down
Loading