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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ frappe.ui.form.on('DFP External Storage', {
frm.button_remote_files_list = null
},

auth_type: function(frm) {
// Toggle visibility of credential fields based on auth type
frm.toggle_reqd('access_key', frm.doc.auth_type === 'Key based')
frm.toggle_reqd('secret_key', frm.doc.auth_type === 'Key based')
frm.refresh_field('access_key')
frm.refresh_field('secret_key')
},

refresh: function(frm) {
// Set initial required state based on auth_type
if (frm.doc.auth_type) {
frm.toggle_reqd('access_key', frm.doc.auth_type === 'Key based')
frm.toggle_reqd('secret_key', frm.doc.auth_type === 'Key based')
}
if (frm.is_new() && !frm.doc.doctypes_ignored.length) {
frm.doc.doctypes_ignored.push({doctype_to_ignore: 'Data Import'})
frm.doc.doctypes_ignored.push({doctype_to_ignore: 'Prepared Report'})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"secure",
"bucket_name",
"region",
"auth_type",
"access_key",
"secret_key",
"folders",
Expand Down Expand Up @@ -44,6 +45,25 @@
"reqd": 1
},
{
"default": "auto",
"description": "Region name of buckets in S3 service. Default value: \"auto\".",
"documentation_url": "https://min.io/docs/minio/linux/developers/python/API.html",
"fieldname": "region",
"fieldtype": "Data",
"label": "Region",
"reqd": 1
},
{
"default": "Key based",
"description": "Authentication method to use. 'Key based' requires Access Key and Secret Key. You can also optionally provide session token if you have short-lived credentials. 'Aws Iam' to use AWS IAM credentials (e.g., from AWS credentials file or IAM role).",
"fieldname": "auth_type",
"fieldtype": "Select",
"label": "Authentication Type",
"options": "Key based\nAws Iam",
"reqd": 1
},
{
"depends_on": "eval:doc.auth_type == 'Key based'",
"description": "Access key (aka user ID) of your account in S3 service. If empty, will use environment variables (AWS_ACCESS_KEY_ID or MINIO_ACCESS_KEY or MINIO_ROOT_USER).",
"documentation_url": "https://min.io/docs/minio/linux/developers/python/API.html",
"fieldname": "access_key",
Expand All @@ -52,22 +72,14 @@
"label": "Access Key"
},
{
"depends_on": "eval:doc.auth_type == 'Key based'",
"description": "Secret Key (aka password) of your account in S3 service. If empty, will use environment variables (AWS_SECRET_ACCESS_KEY or MINIO_SECRET_KEY or MINIO_ROOT_PASSWORD).",
"documentation_url": "https://min.io/docs/minio/linux/developers/python/API.html",
"fieldname": "secret_key",
"fieldtype": "Password",
"label": "Secret Key",
"no_copy": 1
},
{
"default": "auto",
"description": "Region name of buckets in S3 service. Default value: \"auto\".",
"documentation_url": "https://min.io/docs/minio/linux/developers/python/API.html",
"fieldname": "region",
"fieldtype": "Data",
"label": "Region",
"reqd": 1
},
{
"description": "<strong>Be careful!</strong> Please set up your bucket as private in your provider and/or remove public access to objects. <strong>Some files you upload to Frappe will be for sure private!</strong>",
"documentation_url": "https://min.io/docs/minio/linux/developers/python/API.html",
Expand All @@ -84,7 +96,7 @@
{
"fieldname": "html_1",
"fieldtype": "HTML",
"options": "<p>Used Minio library (Simple Storage Service or S3) client to perform bucket and object operations), so please refer to it class initialization for more info:</p>\n<ul>\n<li><strong>endpoint</strong>: Hostname of a S3 service.</li>\n<li><strong>access_key</strong>: Access key (aka user ID) of your account in S3 service.</li>\n<li><strong>secret_key</strong>: Secret Key (aka password) of your account in S3 service.</li>\n<li><strong>session_token</strong>: Session token of your account in S3 service.</li>\n<li><strong>secure</strong>: Flag to indicate to use secure (TLS) connection to S3 service or not.</li>\n</ul>"
"options": "<p>Used Minio library (Simple Storage Service or S3) client to perform bucket and object operations), so please refer to it class initialization for more info:</p>\n<ul>\n<li><strong>endpoint</strong>: Hostname of a S3 service.</li>\n<li><strong>access_key</strong>: Access key (aka user ID) of your account in S3 service.</li>\n<li><strong>secret_key</strong>: Secret Key (aka password) of your account in S3 service.</li>\n<li><strong>secure</strong>: Flag to indicate to use secure (TLS) connection to S3 service or not.</li>\n</ul>"
},
{
"description": "Only host and port. For example:\n<ul><li>[id].r2.cloudflarestorage.com:443</li><li>s3.amazonaws.com</li><li>minio:9000</li><li>...</li></ul>\nRead <a href=\"https://min.io/docs/minio/linux/developers/python/API.html\" target=\"_blank\">Minio documentation</a> for more info.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from frappe.core.doctype.file.file import URL_PREFIXES
from frappe.model.document import Document
from frappe.utils.password import get_decrypted_password

from minio.credentials.providers import IamAwsProvider
from minio.credentials.providers import StaticProvider

DFP_EXTERNAL_STORAGE_PUBLIC_CACHE_PREFIX = "external_storage_public_file:"

Expand All @@ -25,9 +26,9 @@


DFP_EXTERNAL_STORAGE_CONNECTION_FIELDS = [
"type", "endpoint", "secure", "bucket_name", "region", "access_key", "secret_key"]
"type", "endpoint", "secure", "bucket_name", "region", "auth_type", "access_key", "secret_key"]
DFP_EXTERNAL_STORAGE_CRITICAL_FIELDS = [
"type", "endpoint", "secure", "bucket_name", "region", "access_key", "secret_key", "folders"]
"type", "endpoint", "secure", "bucket_name", "region", "auth_type", "access_key", "secret_key", "folders"]


class S3FileProxy:
Expand Down Expand Up @@ -124,51 +125,73 @@ def validate_bucket(self):

@cached_property
def client(self):
# Allow access_key/secret_key to be optional: if not provided in this DocType,
# fallback to environment variables.
if self.endpoint and self.region:
if not self.endpoint or not self.region:
return None

# Handle IAM authentication
if getattr(self, 'auth_type', 'Key based') == 'Aws Iam':
try:
# Resolve access key
access_key = self.access_key or \
os.getenv("AWS_ACCESS_KEY_ID") or \
os.getenv("MINIO_ACCESS_KEY") or \
os.getenv("MINIO_ROOT_USER")

# Resolve secret key
if self.is_new() and self.secret_key:
key_secret = self.secret_key
elif self.secret_key:
key_secret = get_decrypted_password("DFP External Storage", self.name, "secret_key") if self.name else None
else:
key_secret = None

secret_key = key_secret or \
os.getenv("AWS_SECRET_ACCESS_KEY") or \
os.getenv("MINIO_SECRET_KEY") or \
os.getenv("MINIO_ROOT_PASSWORD")

if access_key and secret_key:
credentials = IamAwsProvider(
region=self.region,
)
if credentials:
return MinioConnection(
endpoint=self.endpoint,
access_key=access_key,
secret_key=secret_key,
credentials=credentials,
region=self.region,
secure=self.secure,
)
except Exception:
except:
pass

# Handle Key based authentication
# Allow access_key/secret_key to be optional: if not provided in this DocType,
# fallback to environment variables.
try:
# Resolve access key
access_key = self.access_key or \
os.getenv("AWS_ACCESS_KEY_ID") or \
os.getenv("MINIO_ACCESS_KEY") or \
os.getenv("MINIO_ROOT_USER")

# Resolve secret key
if self.is_new() and self.secret_key:
key_secret = self.secret_key
elif self.secret_key:
key_secret = get_decrypted_password("DFP External Storage", self.name, "secret_key") if self.name else None
else:
key_secret = None

secret_key = key_secret or \
os.getenv("AWS_SECRET_ACCESS_KEY") or \
os.getenv("MINIO_SECRET_KEY") or \
os.getenv("MINIO_ROOT_PASSWORD")

credentials = StaticProvider(
access_key=access_key,
secret_key=secret_key,
)
return MinioConnection(
endpoint=self.endpoint,
credentials=credentials,
region=self.region,
secure=self.secure,
)
except Exception:
pass

return None

def remote_files_list(self):
return self.client.list_objects(self.bucket_name, recursive=True)


class MinioConnection:
def __init__(self, endpoint:str, access_key:str, secret_key:str, region:str, secure:bool):
def __init__(self, endpoint:str, region:str, credentials:StaticProvider | IamAwsProvider, secure:bool):
self.client = Minio(
endpoint=endpoint,
access_key=access_key,
secret_key=secret_key,
region=region,
credentials=credentials,
secure=secure,
)

Expand Down