Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor MinioAdminClient using HTTP client #1291

Merged
merged 27 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8c71fd2
Update deprecated mc admin commands
pbrw Apr 25, 2023
6f65a71
Merge branch 'master' into master
harshavardhana May 19, 2023
5dd8726
Merge branch 'master' into master
balamurugana May 19, 2023
5cd16c1
Merge branch 'master' into master
balamurugana May 21, 2023
26f9aff
Merge branch 'minio:master' into master
pbrw Jul 2, 2023
74ea6f2
Merge branch 'minio:master' into master
pbrw Aug 21, 2023
aca0955
Cryptography to read/write encrypted MinIO Admin payload
pbrw Jul 24, 2023
26b1258
Basic MinIO user management API
pbrw Aug 21, 2023
e2b3c90
User: Add, Info, Remove, List
pbrw Aug 21, 2023
b2e92e9
simplify api execution
balamurugana Aug 23, 2023
e502e18
fix retrieve credentials
pbrw Aug 24, 2023
a0a5563
Policy management commands
pbrw Aug 24, 2023
0d3e28e
Group management commands
pbrw Aug 25, 2023
be865a9
User enable/disable commands
pbrw Aug 25, 2023
fa13450
Service commands
pbrw Aug 25, 2023
e7a64bc
Service, update, info, top locks commands
pbrw Aug 25, 2023
e59936d
Config commands
pbrw Aug 28, 2023
68f610a
Remove unnecessary 'else'
pbrw Aug 29, 2023
255da0e
Quota, Profile commands
pbrw Aug 29, 2023
3660126
KMS key status commands
pbrw Aug 29, 2023
cd02b42
Typo fix
pbrw Sep 19, 2023
cfad703
Merge branch 'master' into minio-admin-http
harshavardhana Sep 25, 2023
a67ffbf
Prometheus generate command
pbrw Sep 26, 2023
c60cd4a
Revert "Prometheus generate command"
balamurugana Sep 26, 2023
518f659
add site replication apis
balamurugana Sep 26, 2023
d1b3cab
Merge branch 'master' into minio-admin-http
pbrw Sep 26, 2023
5bd31ac
Typo fix
pbrw Sep 27, 2023
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: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools
pip install certifi urllib3 mock pytest
pip install certifi urllib3 mock pytest argon2-cffi pycryptodome
- name: Run check if Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ default: tests

getdeps:
@echo "Installing required dependencies"
@pip install --user --upgrade autopep8 certifi pytest pylint urllib3
@pip install --user --upgrade autopep8 certifi pytest pylint urllib3 argon2-cffi pycryptodome

check: getdeps
@echo "Running checks"
Expand Down
15 changes: 5 additions & 10 deletions minio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import itertools
import os
import platform
import tarfile
from datetime import timedelta
from io import BytesIO
Expand All @@ -47,10 +46,11 @@
parse_copy_object, parse_list_objects)
from .deleteobjects import DeleteError, DeleteRequest, DeleteResult
from .error import InvalidResponseError, S3Error, ServerError
from .helpers import (MAX_MULTIPART_COUNT, MAX_MULTIPART_OBJECT_SIZE,
MAX_PART_SIZE, MIN_PART_SIZE, BaseURL, ObjectWriteResult,
ThreadPool, check_bucket_name, check_non_empty_string,
check_sse, check_ssec, genheaders, get_part_info,
from .helpers import (_DEFAULT_USER_AGENT, MAX_MULTIPART_COUNT,
MAX_MULTIPART_OBJECT_SIZE, MAX_PART_SIZE, MIN_PART_SIZE,
BaseURL, ObjectWriteResult, ThreadPool,
check_bucket_name, check_non_empty_string, check_sse,
check_ssec, genheaders, get_part_info,
headers_to_strings, is_valid_policy_type, makedirs,
md5sum_hash, read_part_data, sha256_hash)
from .legalhold import LegalHold
Expand All @@ -67,11 +67,6 @@
from .versioningconfig import VersioningConfig
from .xml import Element, SubElement, findtext, getbytes, marshal, unmarshal

_DEFAULT_USER_AGENT = (
f"MinIO ({platform.system()}; {platform.machine()}) "
f"{__title__}/{__version__}"
)


class Minio: # pylint: disable=too-many-public-methods
"""
Expand Down
146 changes: 146 additions & 0 deletions minio/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C)
# 2015, 2016, 2017 MinIO, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=too-many-lines,disable=too-many-branches,too-many-statements
# pylint: disable=too-many-arguments

"""Cryptography to read and write encrypted MinIO Admin payload"""

import os
pbrw marked this conversation as resolved.
Show resolved Hide resolved

from argon2.low_level import Type, hash_secret_raw
from Crypto.Cipher import AES, ChaCha20_Poly1305

_NONCE_LEN = 8
_SALT_LEN = 32


class AesGcmCipherProvider:
"""AES-GCM cipher provider"""
@staticmethod
def get_cipher(key: bytes, nonce: bytes):
"""Get cipher"""
return AES.new(key, AES.MODE_GCM, nonce)


class ChaCha20Poly1305CipherProvider:
"""ChaCha20Poly1305 cipher provider"""
@staticmethod
def get_cipher(key: bytes, nonce: bytes):
"""Get cipher"""
return ChaCha20_Poly1305.new(key=key, nonce=nonce)


def encrypt(payload: bytes, password: str) -> bytes:
"""
Encrypts data using AES-GCM using a 256-bit Argon2ID key.
To see the original implementation in Go, check out the madmin-go library
(https://github.com/minio/madmin-go/blob/main/encrypt.go#L38)
"""
cipher_provider = AesGcmCipherProvider()
nonce = os.urandom(_NONCE_LEN)
salt = os.urandom(_SALT_LEN)

padded_nonce = [0] * (_NONCE_LEN + 4)
padded_nonce[:_NONCE_LEN] = nonce

key = _generate_key(password.encode(), salt)
additional_data = _generate_additional_data(
cipher_provider, key, bytes(padded_nonce))

padded_nonce[8] = 0x01
padded_nonce = bytes(padded_nonce)

cipher = cipher_provider.get_cipher(key, padded_nonce)
cipher.update(additional_data)
encrypted_data, mac = cipher.encrypt_and_digest(payload)

payload = salt
payload += bytes([0x00])
payload += nonce
payload += encrypted_data
payload += mac

return bytes(payload)


def decrypt(payload: bytes, password: str) -> bytes:
"""
Decrypts data using AES-GCM or ChaCha20Poly1305 using a
256-bit Argon2ID key. To see the original implementation in Go,
check out the madmin-go library
(https://github.com/minio/madmin-go/blob/main/encrypt.go#L38)
"""
pos = 0
salt = payload[pos:pos+_SALT_LEN]
pos += _SALT_LEN

cipher_id = payload[pos]
if cipher_id == 0:
cipher_provider = AesGcmCipherProvider()
elif cipher_id == 1:
cipher_provider = ChaCha20Poly1305CipherProvider()
else:
return None

pos += 1

nonce = payload[pos:pos+_NONCE_LEN]
pos += _NONCE_LEN

encrypted_data = payload[pos:-16]
hmac_tag = payload[-16:]

key = _generate_key(password.encode(), salt)

padded_nonce = [0] * 12
padded_nonce[:_NONCE_LEN] = nonce

additional_data = _generate_additional_data(
cipher_provider, key, bytes(padded_nonce))
padded_nonce[8] = 1

cipher = cipher_provider.get_cipher(key, bytes(padded_nonce))

cipher.update(additional_data)
decrypted_data = cipher.decrypt_and_verify(encrypted_data, hmac_tag)

return decrypted_data


def _generate_additional_data(cipher_provider, key: bytes,
padded_nonce: bytes) -> bytes:
"""Generate additional data"""
cipher = cipher_provider.get_cipher(key, padded_nonce)
tag = cipher.digest()
new_tag = [0] * 17
new_tag[1:] = tag
new_tag[0] = 0x80
return bytes(new_tag)


def _generate_key(password: bytes, salt: bytes) -> bytes:
"""Generate 256-bit Argon2ID key"""
return hash_secret_raw(
secret=password,
salt=salt,
time_cost=1,
memory_cost=65536,
parallelism=4,
hash_len=32,
type=Type.ID,
version=19
)
14 changes: 14 additions & 0 deletions minio/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,17 @@ def copy(self, code, message):
self._bucket_name,
self._object_name,
)


class MinioAdminException(Exception):
"""Raised to indicate admin API execution error."""

def __init__(self, code, body):
self._code = code
self._body = body
super().__init__(
f"admin request failed; Status: {code}, Body: {body}",
)

def __reduce__(self):
return type(self), (self._code, self._body)
75 changes: 45 additions & 30 deletions minio/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@
import hashlib
import math
import os
import platform
import re
import urllib.parse
from queue import Queue
from threading import BoundedSemaphore, Thread

from . import __title__, __version__
from .sse import Sse, SseCustomerKey
from .time import to_iso8601utc

# Constants
_DEFAULT_USER_AGENT = (
f"MinIO ({platform.system()}; {platform.machine()}) "
f"{__title__}/{__version__}"
)

MAX_MULTIPART_COUNT = 10000 # 10000 parts
MAX_MULTIPART_OBJECT_SIZE = 5 * 1024 * 1024 * 1024 * 1024 # 5TiB
MAX_PART_SIZE = 5 * 1024 * 1024 * 1024 # 5GiB
Expand Down Expand Up @@ -427,53 +434,61 @@ def _get_aws_info(host, https, region):
"dualstack": dualstack}, None)


class BaseURL:
"""Base URL of S3 endpoint."""
def _parse_url(endpoint):
"""Parse url string."""

def __init__(self, endpoint, region):
url = urllib.parse.urlsplit(endpoint)
host = url.hostname
url = urllib.parse.urlsplit(endpoint)
host = url.hostname

if url.scheme.lower() not in ["http", "https"]:
raise ValueError("scheme in endpoint must be http or https")
if url.scheme.lower() not in ["http", "https"]:
raise ValueError("scheme in endpoint must be http or https")

url = url_replace(url, scheme=url.scheme.lower())
url = url_replace(url, scheme=url.scheme.lower())

if url.path and url.path != "/":
raise ValueError("path in endpoint is not allowed")
if url.path and url.path != "/":
raise ValueError("path in endpoint is not allowed")

url = url_replace(url, path="")
url = url_replace(url, path="")

if url.query:
raise ValueError("query in endpoint is not allowed")
if url.query:
raise ValueError("query in endpoint is not allowed")

if url.fragment:
raise ValueError("fragment in endpoint is not allowed")
if url.fragment:
raise ValueError("fragment in endpoint is not allowed")

try:
url.port
except ValueError as exc:
raise ValueError("invalid port") from exc
try:
url.port
except ValueError as exc:
raise ValueError("invalid port") from exc

if url.username:
raise ValueError("username in endpoint is not allowed")
if url.username:
raise ValueError("username in endpoint is not allowed")

if url.password:
raise ValueError("password in endpoint is not allowed")
if url.password:
raise ValueError("password in endpoint is not allowed")

if (
(url.scheme == "http" and url.port == 80) or
(url.scheme == "https" and url.port == 443)
):
url = url_replace(url, netloc=host)
if (
(url.scheme == "http" and url.port == 80) or
(url.scheme == "https" and url.port == 443)
):
url = url_replace(url, netloc=host)

return url


class BaseURL:
"""Base URL of S3 endpoint."""

def __init__(self, endpoint, region):
url = _parse_url(endpoint)

if region and not _REGION_REGEX.match(region):
raise ValueError(f"invalid region {region}")

self._aws_info, region_in_host = _get_aws_info(
host, url.scheme == "https", region)
url.hostname, url.scheme == "https", region)
self._virtual_style_flag = (
self._aws_info or host.endswith("aliyuncs.com")
self._aws_info or url.hostname.endswith("aliyuncs.com")
)
self._url = url
self._region = region or region_in_host
Expand Down
Loading