From bed565211f104f8c6c15b33a0223163cd5dafcff Mon Sep 17 00:00:00 2001 From: Deepanjan Bhattacharyya Date: Wed, 20 Nov 2024 13:42:07 -0800 Subject: [PATCH] add resource permission to lambda --- .../gyro/aws/lambda/FunctionPermission.java | 237 ++++++++++++++++++ .../gyro/aws/lambda/FunctionResource.java | 80 +++++- .../java/gyro/aws/lambda/LayerPermission.java | 161 ++++++++++++ .../java/gyro/aws/lambda/LayerResource.java | 71 +++++- 4 files changed, 547 insertions(+), 2 deletions(-) create mode 100644 src/main/java/gyro/aws/lambda/FunctionPermission.java create mode 100644 src/main/java/gyro/aws/lambda/LayerPermission.java diff --git a/src/main/java/gyro/aws/lambda/FunctionPermission.java b/src/main/java/gyro/aws/lambda/FunctionPermission.java new file mode 100644 index 000000000..17ae969f7 --- /dev/null +++ b/src/main/java/gyro/aws/lambda/FunctionPermission.java @@ -0,0 +1,237 @@ +package gyro.aws.lambda; + +import java.util.Iterator; + +import com.fasterxml.jackson.databind.JsonNode; +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.lambda.model.AddPermissionRequest; +import software.amazon.awssdk.services.lambda.model.FunctionUrlAuthType; + +public class FunctionPermission extends Diffable implements Copyable { + + private String functionName; + private String statementId; + private String action; + private String principal; + private String sourceArn; + private String sourceAccount; + private String eventSourceToken; + private String qualifier; + private String revisionId; + private String principalOrgID; + private FunctionUrlAuthType functionUrlAuthType; + + /** + * The name of the Lambda function. + */ + @Updatable + public String getFunctionName() { + return functionName; + } + + public void setFunctionName(String functionName) { + this.functionName = functionName; + } + + /** + * A unique statement identifier. + */ + @Required + public String getStatementId() { + return statementId; + } + + public void setStatementId(String statementId) { + this.statementId = statementId; + } + + /** + * The action that the principal can perform. + */ + @Updatable + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + /** + * The principal who is getting this permission. + */ + @Updatable + public String getPrincipal() { + return principal; + } + + public void setPrincipal(String principal) { + this.principal = principal; + } + + /** + * The ARN of the source that is invoking the function. + */ + @Updatable + public String getSourceArn() { + return sourceArn; + } + + public void setSourceArn(String sourceArn) { + this.sourceArn = sourceArn; + } + + /** + * The AWS account ID of the source that is invoking the function. + */ + @Updatable + public String getSourceAccount() { + return sourceAccount; + } + + public void setSourceAccount(String sourceAccount) { + this.sourceAccount = sourceAccount; + } + + /** + * The token that must be present in the request. + */ + @Updatable + public String getEventSourceToken() { + return eventSourceToken; + } + + public void setEventSourceToken(String eventSourceToken) { + this.eventSourceToken = eventSourceToken; + } + + /** + * The version or alias of the function. + */ + @Updatable + public String getQualifier() { + return qualifier; + } + + public void setQualifier(String qualifier) { + this.qualifier = qualifier; + } + + /** + * The revision ID of the function. + */ + @Updatable + public String getRevisionId() { + return revisionId; + } + + public void setRevisionId(String revisionId) { + this.revisionId = revisionId; + } + + /** + * The organization ID of the principal. + */ + @Updatable + public String getPrincipalOrgID() { + return principalOrgID; + } + + public void setPrincipalOrgID(String principalOrgID) { + this.principalOrgID = principalOrgID; + } + + /** + * The type of authentication to use. + */ + @Updatable + public FunctionUrlAuthType getFunctionUrlAuthType() { + return functionUrlAuthType; + } + + public void setFunctionUrlAuthType(FunctionUrlAuthType functionUrlAuthType) { + this.functionUrlAuthType = functionUrlAuthType; + } + + @Override + public String primaryKey() { + return getStatementId(); + } + + @Override + public void copyFrom(AddPermissionRequest model) { + setFunctionName(model.functionName()); + setStatementId(model.statementId()); + setAction(model.action()); + setPrincipal(model.principal()); + setSourceArn(model.sourceArn()); + setSourceAccount(model.sourceAccount()); + setEventSourceToken(model.eventSourceToken()); + setQualifier(model.qualifier()); + setRevisionId(model.revisionId()); + setPrincipalOrgID(model.principalOrgID()); + setFunctionUrlAuthType(model.functionUrlAuthType()); + } + + AddPermissionRequest toAddPermissionRequest() { + return AddPermissionRequest.builder() + .functionName(getFunctionName()) + .statementId(getStatementId()) + .action(getAction()) + .principal(getPrincipal()) + .sourceArn(getSourceArn()) + .sourceAccount(getSourceAccount()) + .eventSourceToken(getEventSourceToken()) + .qualifier(getQualifier()) + .revisionId(getRevisionId()) + .principalOrgID(getPrincipalOrgID()) + .functionUrlAuthType(getFunctionUrlAuthType()) + .build(); + } + + protected static AddPermissionRequest getAddPermissionRequest(JsonNode statement) { + AddPermissionRequest.Builder builder = AddPermissionRequest.builder(); + + builder.statementId(statement.get("Sid").asText()) + .action(statement.get("Action").asText()) + .principal(statement.get("Principal").get("Service").asText()) + .functionName(statement.get("Resource").asText()); + + if (statement.has("Condition")) { + JsonNode condition = statement.get("Condition"); + Iterator fieldNames = condition.fieldNames(); + + while (fieldNames.hasNext()) { + String conditionKey = fieldNames.next(); + JsonNode conditionValue = condition.get(conditionKey); + + if (conditionKey.equals("ArnLike") && conditionValue.has("AWS:SourceArn")) { + builder.sourceArn(conditionValue.get("AWS:SourceArn").asText()); + } else if (conditionKey.equals("StringEquals") && conditionValue.has("AWS:SourceAccount")) { + builder.sourceAccount(conditionValue.get("AWS:SourceAccount").asText()); + } + } + } + + if (statement.has("Qualifier")) { + builder.qualifier(statement.get("Qualifier").asText()); + } + if (statement.has("FunctionUrlAuthType")) { + builder.functionUrlAuthType(FunctionUrlAuthType.fromValue(statement.get("FunctionUrlAuthType").asText())); + } + if (statement.has("EventSourceToken")) { + builder.eventSourceToken(statement.get("EventSourceToken").asText()); + } + if (statement.has("RevisionId")) { + builder.revisionId(statement.get("RevisionId").asText()); + } + if (statement.has("PrincipalOrgID")) { + builder.principalOrgID(statement.get("PrincipalOrgID").asText()); + } + + return builder.build(); + } +} diff --git a/src/main/java/gyro/aws/lambda/FunctionResource.java b/src/main/java/gyro/aws/lambda/FunctionResource.java index ede231e4d..7cb3a72c3 100644 --- a/src/main/java/gyro/aws/lambda/FunctionResource.java +++ b/src/main/java/gyro/aws/lambda/FunctionResource.java @@ -16,6 +16,8 @@ package gyro.aws.lambda; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.psddev.dari.util.ObjectUtils; import gyro.aws.AwsResource; import gyro.aws.Copyable; @@ -38,10 +40,12 @@ import org.apache.commons.codec.digest.DigestUtils; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.awssdk.services.lambda.model.AddPermissionRequest; import software.amazon.awssdk.services.lambda.model.CreateFunctionRequest; import software.amazon.awssdk.services.lambda.model.CreateFunctionResponse; import software.amazon.awssdk.services.lambda.model.FunctionConfiguration; import software.amazon.awssdk.services.lambda.model.GetFunctionResponse; +import software.amazon.awssdk.services.lambda.model.GetPolicyResponse; import software.amazon.awssdk.services.lambda.model.ListTagsResponse; import software.amazon.awssdk.services.lambda.model.ListVersionsByFunctionResponse; import software.amazon.awssdk.services.lambda.model.ResourceNotFoundException; @@ -107,6 +111,7 @@ public class FunctionResource extends AwsResource implements Copyable versionMap; private Boolean publish; + private Set permission; // -- Readonly @@ -438,6 +443,22 @@ public void setPublish(Boolean publish) { this.publish = publish; } + /** + * The set of permissions to be associated with the Lambda Function. + */ + @Updatable + public Set getPermission() { + if (permission == null) { + permission = new HashSet<>(); + } + + return permission; + } + + public void setPermission(Set permission) { + this.permission = permission; + } + /** * The arn for the lambda Lambda Function resource including the version. */ @@ -571,6 +592,17 @@ public void copyFrom(FunctionConfiguration configuration) { ); setReservedConcurrentExecutions(response.concurrency() != null ? response.concurrency().reservedConcurrentExecutions() : null); + + getPermission().clear(); + try { + GetPolicyResponse policy = client.getPolicy(r -> r.functionName(getName())); + + if (policy.policy() != null) { + setPolicy(policy.policy()); + } + } catch (ResourceNotFoundException ex) { + // Ignore + } } @Override @@ -647,18 +679,29 @@ public void create(GyroUI ui, State state) { setVersion(response.version()); setCodeHash(response.codeSha256()); + setVersions(client); + state.save(); + if (getReservedConcurrentExecutions() != null) { try { client.putFunctionConcurrency( r -> r.functionName(getName()) .reservedConcurrentExecutions(getReservedConcurrentExecutions()) ); + + state.save(); } catch (Exception ex) { ui.write("\n@|bold,red Error assigning reserved concurrency executions to lambda function %s. Error - %s|@", getArn(), ex.getMessage()); } } - setVersions(client); + if (!getPermission().isEmpty()) { + for (FunctionPermission permission : getPermission()) { + client.addPermission(permission.toAddPermissionRequest()); + } + + state.save(); + } } @Override @@ -671,6 +714,23 @@ public void update(GyroUI ui, State state, Resource resource, Set change Set changeSet = new HashSet<>(changedFieldNames); + if (changeSet.contains("permission")) { + if (!oldResource.getPermission().isEmpty()) { + for (FunctionPermission permission : oldResource.getPermission()) { + client.removePermission( + r -> r.functionName(getName()) + .statementId(permission.getStatementId()) + ); + } + } + + for (FunctionPermission permission : getPermission()) { + client.addPermission(permission.toAddPermissionRequest()); + } + + changeSet.remove("permission"); + } + if (changeSet.contains("reserved-concurrent-executions")) { if (getReservedConcurrentExecutions() != null) { client.putFunctionConcurrency( @@ -820,4 +880,22 @@ private void setVersions(LambdaClient client) { } } } + + private void setPolicy(String jsonPolicy) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode policyNode = objectMapper.readTree(jsonPolicy); + JsonNode statements = policyNode.get("Statement"); + + for (JsonNode statement : statements) { + AddPermissionRequest request = FunctionPermission.getAddPermissionRequest(statement); + FunctionPermission permission = newSubresource(FunctionPermission.class); + permission.copyFrom(request); + getPermission().add(permission); + } + + } catch (Exception e) { + throw new GyroException("Error parsing function policy",e); + } + } } diff --git a/src/main/java/gyro/aws/lambda/LayerPermission.java b/src/main/java/gyro/aws/lambda/LayerPermission.java new file mode 100644 index 000000000..0bc34ea7a --- /dev/null +++ b/src/main/java/gyro/aws/lambda/LayerPermission.java @@ -0,0 +1,161 @@ +package gyro.aws.lambda; + +import com.fasterxml.jackson.databind.JsonNode; +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.lambda.model.AddLayerVersionPermissionRequest; + +public class LayerPermission extends Diffable implements Copyable { + + private String layerName; + private Long versionNumber; + private String organizationId; + private String principal; + private String statementId; + private String action; + private String revisionId; + + /** + * The name of the layer. + */ + @Required + public String getLayerName() { + return layerName; + } + + public void setLayerName(String layerName) { + this.layerName = layerName; + } + + /** + * The version number of the layer. + */ + @Updatable + public Long getVersionNumber() { + return versionNumber; + } + + public void setVersionNumber(Long versionNumber) { + this.versionNumber = versionNumber; + } + + /** + * The AWS organization ID. + */ + @Updatable + public String getOrganizationId() { + return organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + + /** + * The principal who is getting this permission. + */ + @Updatable + public String getPrincipal() { + return principal; + } + + public void setPrincipal(String principal) { + this.principal = principal; + } + + /** + * A unique statement identifier. + */ + @Required + public String getStatementId() { + return statementId; + } + + public void setStatementId(String statementId) { + this.statementId = statementId; + } + + /** + * The action that the principal can perform. + */ + @Updatable + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + /** + * The revision ID of the layer. + */ + @Updatable + public String getRevisionId() { + return revisionId; + } + + public void setRevisionId(String revisionId) { + this.revisionId = revisionId; + } + + @Override + public void copyFrom(AddLayerVersionPermissionRequest model) { + setLayerName(model.layerName()); + setVersionNumber(model.versionNumber()); + setOrganizationId(model.organizationId()); + setPrincipal(model.principal()); + setStatementId(model.statementId()); + setAction(model.action()); + setRevisionId(model.revisionId()); + } + + @Override + public String primaryKey() { + return ""; + } + + AddLayerVersionPermissionRequest toAddLayerVersionPermissionRequest() { + return AddLayerVersionPermissionRequest.builder() + .layerName(getLayerName()) + .versionNumber(getVersionNumber()) + .organizationId(getOrganizationId()) + .principal(getPrincipal()) + .statementId(getStatementId()) + .action(getAction()) + .revisionId(getRevisionId()) + .build(); + } + + protected static AddLayerVersionPermissionRequest getAddLayerPermissionRequest(JsonNode statement) { + AddLayerVersionPermissionRequest.Builder builder = AddLayerVersionPermissionRequest.builder(); + + builder.statementId(statement.get("Sid").asText()) + .action(statement.get("Action").asText()) + .layerName(extractLayerName(statement.get("Resource").asText())) + .versionNumber(extractVersionNumber(statement.get("Resource").asText())); + + if (statement.has("Principal")) { + builder.principal(statement.get("Principal").get("AWS").asText()); + } + + if (statement.has("PrincipalOrgID")) { + builder.organizationId(statement.get("PrincipalOrgID").asText()); + } + + return builder.build(); + } + + private static String extractLayerName(String resourceArn) { + String[] parts = resourceArn.split(":"); + return parts[6]; + } + + private static Long extractVersionNumber(String resourceArn) { + String[] parts = resourceArn.split(":"); + return Long.parseLong(parts[7]); + } + +} diff --git a/src/main/java/gyro/aws/lambda/LayerResource.java b/src/main/java/gyro/aws/lambda/LayerResource.java index f8047d905..645bd46a7 100644 --- a/src/main/java/gyro/aws/lambda/LayerResource.java +++ b/src/main/java/gyro/aws/lambda/LayerResource.java @@ -16,6 +16,8 @@ package gyro.aws.lambda; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.psddev.dari.util.ObjectUtils; import gyro.aws.AwsResource; import gyro.aws.Copyable; @@ -25,11 +27,14 @@ import gyro.core.resource.Id; import gyro.core.resource.Output; import gyro.core.resource.Resource; +import gyro.core.resource.Updatable; import gyro.core.scope.State; import gyro.core.validation.Required; import gyro.core.validation.ValidStrings; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.awssdk.services.lambda.model.AddLayerVersionPermissionRequest; +import software.amazon.awssdk.services.lambda.model.GetLayerVersionPolicyResponse; import software.amazon.awssdk.services.lambda.model.GetLayerVersionResponse; import software.amazon.awssdk.services.lambda.model.PublishLayerVersionRequest; import software.amazon.awssdk.services.lambda.model.PublishLayerVersionResponse; @@ -69,6 +74,7 @@ public class LayerResource extends AwsResource implements Copyable permission; // -- Readonly @@ -171,6 +177,22 @@ public void setContentZipPath(String contentZipPath) { this.contentZipPath = contentZipPath; } + /** + * The list of permissions for the Lambda Layer. + */ + @Updatable + public Set getPermission() { + if (permission == null) { + permission = new HashSet<>(); + } + + return permission; + } + + public void setPermission(Set permission) { + this.permission = permission; + } + /** * The ARN of the Lambda Layer. */ @@ -229,6 +251,18 @@ public void copyFrom(GetLayerVersionResponse response) { setArn(response.layerArn()); setVersionArn(response.layerVersionArn()); setLicenseInfo(response.licenseInfo()); + + // Set permissions + getPermission().clear(); + LambdaClient client = createClient(LambdaClient.class); + try { + GetLayerVersionPolicyResponse policy = client.getLayerVersionPolicy(r -> r.layerName(getName()) + .versionNumber(getVersion())); + + setPolicy(policy.policy()); + } catch (ResourceNotFoundException ex) { + // Ignore + } } @Override @@ -284,7 +318,23 @@ public void create(GyroUI ui, State state) { @Override public void update(GyroUI ui, State state, Resource resource, Set changedFieldNames) { - + LambdaClient client = createClient(LambdaClient.class); + LayerResource oldResource = (LayerResource) resource; + + if (changedFieldNames.contains("permission")) { + if (!oldResource.getPermission().isEmpty()) { + for (LayerPermission permission : oldResource.getPermission()) { + client.removeLayerVersionPermission(r -> r.layerName(getName()) + .versionNumber(getVersion()) + .statementId(permission.getStatementId()) + ); + } + } + + for (LayerPermission permission : getPermission()) { + client.addLayerVersionPermission(permission.toAddLayerVersionPermissionRequest()); + } + } } @Override @@ -305,4 +355,23 @@ private SdkBytes getZipFile() { throw new GyroException(String.format("File not found - %s",getContentZipPath())); } } + + private void setPolicy(String jsonPolicy) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode policyNode = objectMapper.readTree(jsonPolicy); + JsonNode statements = policyNode.get("Statement"); + + for (JsonNode statement : statements) { + AddLayerVersionPermissionRequest request = LayerPermission.getAddLayerPermissionRequest(statement); + LayerPermission permission = newSubresource(LayerPermission.class); + permission.copyFrom(request); + getPermission().add(permission); + } + + } catch (Exception e) { + throw new GyroException("Error parsing layer policy.", e); + } + } + }