diff --git a/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.js b/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.js index ec59391..5d91c2a 100644 --- a/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.js +++ b/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.js @@ -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'}) diff --git a/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.json b/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.json index c3b2625..99c95de 100644 --- a/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.json +++ b/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.json @@ -16,6 +16,7 @@ "secure", "bucket_name", "region", + "auth_type", "access_key", "secret_key", "folders", @@ -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", @@ -52,6 +72,7 @@ "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", @@ -59,15 +80,6 @@ "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": "Be careful! Please set up your bucket as private in your provider and/or remove public access to objects. Some files you upload to Frappe will be for sure private!", "documentation_url": "https://min.io/docs/minio/linux/developers/python/API.html", @@ -84,7 +96,7 @@ { "fieldname": "html_1", "fieldtype": "HTML", - "options": "

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:

\n" + "options": "

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:

\n" }, { "description": "Only host and port. For example:\n\nRead Minio documentation for more info.", diff --git a/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.py b/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.py index e19aede..bf1f069 100644 --- a/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.py +++ b/dfp_external_storage/dfp_external_storage/doctype/dfp_external_storage/dfp_external_storage.py @@ -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:" @@ -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: @@ -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, )