minioadmin.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016
  1. # -*- coding: utf-8 -*-
  2. # MinIO Python Library for Amazon S3 Compatible Cloud Storage,
  3. # (C) 2021 MinIO, Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. # pylint: disable=too-many-public-methods
  17. # pylint: disable=too-many-lines
  18. """MinIO Admin Client to perform MinIO administration operations."""
  19. from __future__ import absolute_import, annotations
  20. import json
  21. import os
  22. from datetime import timedelta
  23. from enum import Enum, unique
  24. from typing import Any, Optional, TextIO, Tuple, cast
  25. from urllib.parse import urlunsplit
  26. import certifi
  27. from urllib3 import Retry
  28. from urllib3._collections import HTTPHeaderDict
  29. from urllib3.poolmanager import PoolManager
  30. try:
  31. from urllib3.response import BaseHTTPResponse # type: ignore[attr-defined]
  32. except ImportError:
  33. from urllib3.response import HTTPResponse as BaseHTTPResponse
  34. from urllib3.util import Timeout
  35. from . import time
  36. from .credentials import Provider
  37. from .crypto import decrypt, encrypt
  38. from .datatypes import PeerInfo, PeerSite, SiteReplicationStatusOptions
  39. from .error import MinioAdminException
  40. from .helpers import (_DEFAULT_USER_AGENT, _REGION_REGEX, DictType, _parse_url,
  41. headers_to_strings, queryencode, sha256_hash,
  42. url_replace)
  43. from .signer import sign_v4_s3
  44. @unique
  45. class _COMMAND(Enum):
  46. """Admin Command enumerations."""
  47. ACCOUNT_INFO = "accountinfo"
  48. ADD_USER = "add-user"
  49. USER_INFO = "user-info"
  50. LIST_USERS = "list-users"
  51. REMOVE_USER = "remove-user"
  52. SET_USER_STATUS = "set-user-status"
  53. ADD_CANNED_POLICY = "add-canned-policy"
  54. SET_USER_OR_GROUP_POLICY = "set-user-or-group-policy"
  55. LIST_CANNED_POLICIES = "list-canned-policies"
  56. REMOVE_CANNED_POLICY = "remove-canned-policy"
  57. CANNED_POLICY_INFO = "info-canned-policy"
  58. SET_BUCKET_QUOTA = "set-bucket-quota"
  59. GET_BUCKET_QUOTA = "get-bucket-quota"
  60. DATA_USAGE_INFO = "datausageinfo"
  61. ADD_UPDATE_REMOVE_GROUP = "update-group-members"
  62. SET_GROUP_STATUS = "set-group-status"
  63. GROUP_INFO = "group"
  64. LIST_GROUPS = "groups"
  65. INFO = "info"
  66. SERVICE = "service"
  67. UPDATE = "update"
  68. TOP_LOCKS = "top/locks"
  69. HELP_CONFIG = "help-config-kv"
  70. GET_CONFIG = "get-config-kv"
  71. SET_CONFIG = "set-config-kv"
  72. DELETE_CONFIG = "del-config-kv"
  73. LIST_CONFIG_HISTORY = "list-config-history-kv"
  74. RESOTRE_CONFIG_HISTORY = "restore-config-history-kv"
  75. START_PROFILE = "profile"
  76. CREATE_KMS_KEY = "kms/key/create"
  77. GET_KMS_KEY_STATUS = "kms/key/status"
  78. SITE_REPLICATION_ADD = "site-replication/add"
  79. SITE_REPLICATION_INFO = "site-replication/info"
  80. SITE_REPLICATION_STATUS = "site-replication/status"
  81. SITE_REPLICATION_EDIT = "site-replication/edit"
  82. SITE_REPLICATION_REMOVE = "site-replication/remove"
  83. SERVICE_ACCOUNT_INFO = "info-service-account"
  84. SERVICE_ACCOUNT_LIST = "list-service-accounts"
  85. SERVICE_ACCOUNT_ADD = "add-service-account"
  86. SERVICE_ACCOUNT_UPDATE = "update-service-account"
  87. SERVICE_ACCOUNT_DELETE = "delete-service-account"
  88. IDP_LDAP_POLICY_ATTACH = "idp/ldap/policy/attach"
  89. IDP_LDAP_POLICY_DETACH = "idp/ldap/policy/detach"
  90. IDP_LDAP_LIST_ACCESS_KEYS = "idp/ldap/list-access-keys"
  91. IDP_LDAP_LIST_ACCESS_KEYS_BULK = "idp/ldap/list-access-keys-bulk"
  92. IDP_BUILTIN_POLICY_ATTACH = "idp/builtin/policy/attach"
  93. IDP_BUILTIN_POLICY_DETACH = "idp/builtin/policy/detach"
  94. IDP_BUILTIN_POLICY_ENTITIES = "idp/builtin/policy-entities"
  95. def _safe_str(value: Any) -> str:
  96. """Convert to string safely"""
  97. try:
  98. return value.decode() if isinstance(value, bytes) else str(value)
  99. except UnicodeDecodeError:
  100. return value.hex()
  101. class MinioAdmin:
  102. """Client to perform MinIO administration operations."""
  103. def __init__(
  104. self,
  105. *,
  106. endpoint: str,
  107. credentials: Provider,
  108. region: str = "",
  109. secure: bool = True,
  110. cert_check: bool = True,
  111. http_client: Optional[PoolManager] = None,
  112. ):
  113. url = _parse_url(("https://" if secure else "http://") + endpoint)
  114. if not isinstance(credentials, Provider):
  115. raise ValueError("valid credentials must be provided")
  116. if region and not _REGION_REGEX.match(region):
  117. raise ValueError(f"invalid region {region}")
  118. if http_client:
  119. if not isinstance(http_client, PoolManager):
  120. raise ValueError(
  121. "HTTP client should be instance of "
  122. "`urllib3.poolmanager.PoolManager`"
  123. )
  124. else:
  125. timeout = timedelta(minutes=5).seconds
  126. http_client = PoolManager(
  127. timeout=Timeout(connect=timeout, read=timeout),
  128. maxsize=10,
  129. cert_reqs='CERT_REQUIRED' if cert_check else 'CERT_NONE',
  130. ca_certs=os.environ.get('SSL_CERT_FILE') or certifi.where(),
  131. retries=Retry(
  132. total=5,
  133. backoff_factor=0.2,
  134. status_forcelist=[500, 502, 503, 504]
  135. )
  136. )
  137. self._url = url
  138. self._provider = credentials
  139. self._region = region
  140. self._secure = secure
  141. self._cert_check = cert_check
  142. self._http = http_client
  143. self._user_agent = _DEFAULT_USER_AGENT
  144. self._trace_stream: Optional[TextIO] = None
  145. def __del__(self):
  146. self._http.clear()
  147. def _url_open(
  148. self,
  149. *,
  150. method: str,
  151. command: _COMMAND,
  152. query_params: Optional[DictType] = None,
  153. body: Optional[bytes] = None,
  154. preload_content: bool = True,
  155. ) -> BaseHTTPResponse:
  156. """Execute HTTP request."""
  157. creds = self._provider.retrieve()
  158. url = url_replace(url=self._url, path="/minio/admin/v3/"+command.value)
  159. query = []
  160. for key, values in sorted((query_params or {}).items()):
  161. values = values if isinstance(values, (list, tuple)) else [values]
  162. query += [
  163. f"{queryencode(key)}={queryencode(value)}"
  164. for value in sorted(values)
  165. ]
  166. url = url_replace(url=url, query="&".join(query))
  167. content_sha256 = sha256_hash(body)
  168. date = time.utcnow()
  169. headers: DictType = {
  170. "Host": url.netloc,
  171. "User-Agent": self._user_agent,
  172. "x-amz-date": time.to_amz_date(date),
  173. "x-amz-content-sha256": content_sha256,
  174. "Content-Type": "application/octet-stream"
  175. }
  176. if creds.session_token:
  177. headers["X-Amz-Security-Token"] = creds.session_token
  178. if body:
  179. headers["Content-Length"] = str(len(body))
  180. headers = sign_v4_s3(
  181. method=method,
  182. url=url,
  183. region=self._region,
  184. headers=headers,
  185. credentials=creds,
  186. content_sha256=content_sha256,
  187. date=date,
  188. )
  189. if self._trace_stream:
  190. self._trace_stream.write("---------START-HTTP---------\n")
  191. query_string = ("?" + url.query) if url.query else ""
  192. self._trace_stream.write(
  193. f"{method} {url.path}{query_string} HTTP/1.1\n",
  194. )
  195. self._trace_stream.write(
  196. headers_to_strings(headers, titled_key=True),
  197. )
  198. self._trace_stream.write("\n")
  199. if body is not None:
  200. self._trace_stream.write("\n")
  201. self._trace_stream.write(_safe_str(body))
  202. self._trace_stream.write("\n")
  203. self._trace_stream.write("\n")
  204. http_headers = HTTPHeaderDict()
  205. for key, value in headers.items():
  206. if isinstance(value, (list, tuple)):
  207. for val in value:
  208. http_headers.add(key, val)
  209. else:
  210. http_headers.add(key, value)
  211. response = self._http.urlopen(
  212. method,
  213. urlunsplit(url),
  214. body=body,
  215. headers=http_headers,
  216. preload_content=preload_content,
  217. )
  218. if self._trace_stream:
  219. self._trace_stream.write(f"HTTP/1.1 {response.status}\n")
  220. self._trace_stream.write(
  221. headers_to_strings(response.headers),
  222. )
  223. self._trace_stream.write("\n")
  224. if preload_content:
  225. self._trace_stream.write("\n")
  226. self._trace_stream.write(_safe_str(response.data))
  227. self._trace_stream.write("\n")
  228. self._trace_stream.write("----------END-HTTP----------\n")
  229. if response.status in [200, 204, 206]:
  230. return response
  231. raise MinioAdminException(
  232. str(response.status),
  233. _safe_str(response.data),
  234. )
  235. def set_app_info(self, app_name: str, app_version: str):
  236. """
  237. Set your application name and version to user agent header.
  238. :param app_name: Application name.
  239. :param app_version: Application version.
  240. Example::
  241. client.set_app_info('my_app', '1.0.2')
  242. """
  243. if not (app_name and app_version):
  244. raise ValueError("Application name/version cannot be empty.")
  245. self._user_agent = f"{_DEFAULT_USER_AGENT} {app_name}/{app_version}"
  246. def trace_on(self, stream: TextIO):
  247. """
  248. Enable http trace.
  249. :param stream: Stream for writing HTTP call tracing.
  250. """
  251. if not stream:
  252. raise ValueError('Input stream for trace output is invalid.')
  253. # Save new output stream.
  254. self._trace_stream = stream
  255. def trace_off(self):
  256. """
  257. Disable HTTP trace.
  258. """
  259. self._trace_stream = None
  260. def service_restart(self) -> str:
  261. """Restart MinIO service."""
  262. response = self._url_open(
  263. method="POST",
  264. command=_COMMAND.SERVICE,
  265. query_params={"action": "restart"}
  266. )
  267. return response.data.decode()
  268. def service_stop(self) -> str:
  269. """Stop MinIO service."""
  270. response = self._url_open(
  271. method="POST",
  272. command=_COMMAND.SERVICE,
  273. query_params={"action": "stop"}
  274. )
  275. return response.data.decode()
  276. def update(self) -> str:
  277. """Update MinIO."""
  278. response = self._url_open(
  279. method="POST",
  280. command=_COMMAND.UPDATE,
  281. query_params={"updateURL": ""}
  282. )
  283. return response.data.decode()
  284. def info(self) -> str:
  285. """Get MinIO server information."""
  286. response = self._url_open(
  287. method="GET",
  288. command=_COMMAND.INFO,
  289. )
  290. return response.data.decode()
  291. def account_info(self, prefix_usage: bool = False) -> str:
  292. """Get usage information for the authenticating account"""
  293. response = self._url_open(
  294. method="GET",
  295. command=_COMMAND.ACCOUNT_INFO,
  296. query_params={"prefix-usage": "true"} if prefix_usage else None,
  297. )
  298. return response.data.decode()
  299. def user_add(self, access_key: str, secret_key: str) -> str:
  300. """Create user with access and secret keys"""
  301. body = json.dumps(
  302. {"status": "enabled", "secretKey": secret_key}).encode()
  303. response = self._url_open(
  304. method="PUT",
  305. command=_COMMAND.ADD_USER,
  306. query_params={"accessKey": access_key},
  307. body=encrypt(body, self._provider.retrieve().secret_key),
  308. )
  309. return response.data.decode()
  310. def user_disable(self, access_key: str) -> str:
  311. """Disable user."""
  312. response = self._url_open(
  313. method="PUT",
  314. command=_COMMAND.SET_USER_STATUS,
  315. query_params={"accessKey": access_key, "status": "disabled"}
  316. )
  317. return response.data.decode()
  318. def user_enable(self, access_key: str) -> str:
  319. """Enable user."""
  320. response = self._url_open(
  321. method="PUT",
  322. command=_COMMAND.SET_USER_STATUS,
  323. query_params={"accessKey": access_key, "status": "enabled"}
  324. )
  325. return response.data.decode()
  326. def user_remove(self, access_key: str) -> str:
  327. """Delete user"""
  328. response = self._url_open(
  329. method="DELETE",
  330. command=_COMMAND.REMOVE_USER,
  331. query_params={"accessKey": access_key},
  332. )
  333. return response.data.decode()
  334. def user_info(self, access_key: str) -> str:
  335. """Get information about user"""
  336. response = self._url_open(
  337. method="GET",
  338. command=_COMMAND.USER_INFO,
  339. query_params={"accessKey": access_key},
  340. )
  341. return response.data.decode()
  342. def user_list(self) -> str:
  343. """List all users"""
  344. response = self._url_open(
  345. method="GET",
  346. command=_COMMAND.LIST_USERS,
  347. preload_content=False,
  348. )
  349. plain_data = decrypt(
  350. response, self._provider.retrieve().secret_key,
  351. )
  352. return plain_data.decode()
  353. def group_add(self, group_name: str, members: str) -> str:
  354. """Add users a new or existing group."""
  355. body = json.dumps({
  356. "group": group_name,
  357. "members": members,
  358. "isRemove": False
  359. }).encode()
  360. response = self._url_open(
  361. method="PUT",
  362. command=_COMMAND.ADD_UPDATE_REMOVE_GROUP,
  363. body=body,
  364. )
  365. return response.data.decode()
  366. def group_disable(self, group_name: str) -> str:
  367. """Disable group."""
  368. response = self._url_open(
  369. method="PUT",
  370. command=_COMMAND.SET_GROUP_STATUS,
  371. query_params={"group": group_name, "status": "disabled"}
  372. )
  373. return response.data.decode()
  374. def group_enable(self, group_name: str) -> str:
  375. """Enable group."""
  376. response = self._url_open(
  377. method="PUT",
  378. command=_COMMAND.SET_GROUP_STATUS,
  379. query_params={"group": group_name, "status": "enabled"}
  380. )
  381. return response.data.decode()
  382. def group_remove(
  383. self,
  384. group_name: str,
  385. members: Optional[str] = None,
  386. ) -> str:
  387. """Remove group or members from a group."""
  388. data = {
  389. "group": group_name,
  390. "isRemove": True
  391. }
  392. if members is not None:
  393. data["members"] = members
  394. response = self._url_open(
  395. method="PUT",
  396. command=_COMMAND.ADD_UPDATE_REMOVE_GROUP,
  397. body=json.dumps(data).encode(),
  398. )
  399. return response.data.decode()
  400. def group_info(self, group_name: str) -> str:
  401. """Get group information."""
  402. response = self._url_open(
  403. method="GET",
  404. command=_COMMAND.GROUP_INFO,
  405. query_params={"group": group_name},
  406. )
  407. return response.data.decode()
  408. def group_list(self) -> str:
  409. """List groups."""
  410. response = self._url_open(
  411. method="GET",
  412. command=_COMMAND.LIST_GROUPS,
  413. )
  414. return response.data.decode()
  415. def policy_add(self,
  416. policy_name: str,
  417. policy_file: Optional[str | os.PathLike] = None,
  418. policy: Optional[dict] = None) -> str:
  419. """Add new policy."""
  420. if not (policy_file is not None) ^ (policy is not None):
  421. raise ValueError("either policy_file or policy must be provided")
  422. if policy_file:
  423. with open(policy_file, encoding='utf-8') as file:
  424. body = file.read().encode()
  425. else:
  426. body = json.dumps(policy).encode()
  427. response = self._url_open(
  428. method="PUT",
  429. command=_COMMAND.ADD_CANNED_POLICY,
  430. query_params={"name": policy_name},
  431. body=body,
  432. )
  433. return response.data.decode()
  434. def policy_remove(self, policy_name: str) -> str:
  435. """Remove policy."""
  436. response = self._url_open(
  437. method="DELETE",
  438. command=_COMMAND.REMOVE_CANNED_POLICY,
  439. query_params={"name": policy_name},
  440. )
  441. return response.data.decode()
  442. def policy_info(self, policy_name: str) -> str:
  443. """Get policy information."""
  444. response = self._url_open(
  445. method="GET",
  446. command=_COMMAND.CANNED_POLICY_INFO,
  447. query_params={"name": policy_name},
  448. )
  449. return response.data.decode()
  450. def policy_list(self) -> str:
  451. """List policies."""
  452. response = self._url_open(
  453. method="GET",
  454. command=_COMMAND.LIST_CANNED_POLICIES,
  455. )
  456. return response.data.decode()
  457. def policy_set(
  458. self,
  459. policy_name: str,
  460. user: Optional[str] = None,
  461. group: Optional[str] = None,
  462. ) -> str:
  463. """Set IAM policy on a user or group."""
  464. if (user is not None) ^ (group is not None):
  465. response = self._url_open(
  466. method="PUT",
  467. command=_COMMAND.SET_USER_OR_GROUP_POLICY,
  468. query_params={"userOrGroup": cast(str, user or group),
  469. "isGroup": "true" if group else "false",
  470. "policyName": policy_name},
  471. )
  472. return response.data.decode()
  473. raise ValueError("either user or group must be set")
  474. def policy_unset(
  475. self,
  476. policy_name: str | list[str],
  477. user: Optional[str] = None,
  478. group: Optional[str] = None,
  479. ) -> str:
  480. """Unset an IAM policy for a user or group."""
  481. return self.detach_policy(
  482. policy_name if isinstance(policy_name, list) else [policy_name],
  483. user, group)
  484. def config_get(self, key: Optional[str] = None) -> str:
  485. """Get configuration parameters."""
  486. try:
  487. response = self._url_open(
  488. method="GET",
  489. command=_COMMAND.GET_CONFIG,
  490. query_params={"key": key or "", "subSys": ""},
  491. preload_content=False,
  492. )
  493. if key is None:
  494. return response.read().decode()
  495. return decrypt(
  496. response, self._provider.retrieve().secret_key,
  497. ).decode()
  498. finally:
  499. if response:
  500. response.close()
  501. response.release_conn()
  502. def config_set(
  503. self,
  504. key: str,
  505. config: Optional[dict[str, str]] = None,
  506. ) -> str:
  507. """Set configuration parameters."""
  508. data = [key]
  509. if config:
  510. data += [f"{name}={value}" for name, value in config.items()]
  511. body = " ".join(data).encode()
  512. response = self._url_open(
  513. method="PUT",
  514. command=_COMMAND.SET_CONFIG,
  515. body=encrypt(body, self._provider.retrieve().secret_key),
  516. )
  517. return response.data.decode()
  518. def config_reset(self, key: str, name: Optional[str] = None) -> str:
  519. """Reset configuration parameters."""
  520. if name:
  521. key += ":" + name
  522. body = key.encode()
  523. response = self._url_open(
  524. method="DELETE",
  525. command=_COMMAND.DELETE_CONFIG,
  526. body=encrypt(body, self._provider.retrieve().secret_key),
  527. )
  528. return response.data.decode()
  529. def config_history(self) -> str:
  530. """Get historic configuration changes."""
  531. try:
  532. response = self._url_open(
  533. method="GET",
  534. command=_COMMAND.LIST_CONFIG_HISTORY,
  535. query_params={"count": "10"},
  536. preload_content=False,
  537. )
  538. plain_text = decrypt(
  539. response, self._provider.retrieve().secret_key,
  540. )
  541. return plain_text.decode()
  542. finally:
  543. if response:
  544. response.close()
  545. response.release_conn()
  546. def config_restore(self, restore_id: str) -> str:
  547. """Restore to a specific configuration history."""
  548. response = self._url_open(
  549. method="PUT",
  550. command=_COMMAND.RESOTRE_CONFIG_HISTORY,
  551. query_params={"restoreId": restore_id}
  552. )
  553. return response.data.decode()
  554. def profile_start(
  555. self,
  556. profilers: tuple[str] = cast(Tuple[str], ()),
  557. ) -> str:
  558. """Runs a system profile"""
  559. response = self._url_open(
  560. method="POST",
  561. command=_COMMAND.START_PROFILE,
  562. query_params={"profilerType;": ",".join(profilers)},
  563. )
  564. return response.data.decode()
  565. def top_locks(self) -> str:
  566. """Get a list of the 10 oldest locks on a MinIO cluster."""
  567. response = self._url_open(
  568. method="GET",
  569. command=_COMMAND.TOP_LOCKS,
  570. )
  571. return response.data.decode()
  572. def kms_key_create(self, key: Optional[str] = None) -> str:
  573. """Create a new KMS master key."""
  574. response = self._url_open(
  575. method="POST",
  576. command=_COMMAND.CREATE_KMS_KEY,
  577. query_params={"key-id": key or ""},
  578. )
  579. return response.data.decode()
  580. def kms_key_status(self, key: Optional[str] = None) -> str:
  581. """Get status information of a KMS master key."""
  582. response = self._url_open(
  583. method="GET",
  584. command=_COMMAND.GET_KMS_KEY_STATUS,
  585. query_params={"key-id": key or ""}
  586. )
  587. return response.data.decode()
  588. def add_site_replication(self, peer_sites: list[PeerSite]) -> str:
  589. """Add peer sites to site replication."""
  590. body = json.dumps(
  591. [peer_site.to_dict() for peer_site in peer_sites]).encode()
  592. response = self._url_open(
  593. method="PUT",
  594. command=_COMMAND.SITE_REPLICATION_ADD,
  595. query_params={"api-version": "1"},
  596. body=encrypt(body, self._provider.retrieve().secret_key),
  597. )
  598. return response.data.decode()
  599. def get_site_replication_info(self) -> str:
  600. """Get site replication information."""
  601. response = self._url_open(
  602. method="GET",
  603. command=_COMMAND.SITE_REPLICATION_INFO,
  604. )
  605. return response.data.decode()
  606. def get_site_replication_status(
  607. self,
  608. options: SiteReplicationStatusOptions,
  609. ) -> str:
  610. """Get site replication information."""
  611. response = self._url_open(
  612. method="GET",
  613. command=_COMMAND.SITE_REPLICATION_STATUS,
  614. query_params=cast(DictType, options.to_query_params()),
  615. )
  616. return response.data.decode()
  617. def edit_site_replication(self, peer_info: PeerInfo) -> str:
  618. """Edit site replication with given peer information."""
  619. body = json.dumps(peer_info.to_dict()).encode()
  620. response = self._url_open(
  621. method="PUT",
  622. command=_COMMAND.SITE_REPLICATION_EDIT,
  623. query_params={"api-version": "1"},
  624. body=encrypt(body, self._provider.retrieve().secret_key),
  625. )
  626. return response.data.decode()
  627. def remove_site_replication(
  628. self,
  629. sites: Optional[str] = None,
  630. all_sites: bool = False,
  631. ) -> str:
  632. """Remove given sites or all sites from site replication."""
  633. data = {}
  634. if all_sites:
  635. data.update({"all": "True"})
  636. elif sites:
  637. data.update({"sites": sites or ""})
  638. else:
  639. raise ValueError("either sites or all flag must be given")
  640. body = json.dumps(data).encode()
  641. response = self._url_open(
  642. method="PUT",
  643. command=_COMMAND.SITE_REPLICATION_REMOVE,
  644. query_params={"api-version": "1"},
  645. body=encrypt(body, self._provider.retrieve().secret_key),
  646. )
  647. return response.data.decode()
  648. def bucket_quota_set(self, bucket: str, size: int) -> str:
  649. """Set bucket quota configuration."""
  650. body = json.dumps({"quota": size, "quotatype": "hard"}).encode()
  651. response = self._url_open(
  652. method="PUT",
  653. command=_COMMAND.SET_BUCKET_QUOTA,
  654. query_params={"bucket": bucket},
  655. body=body
  656. )
  657. return response.data.decode()
  658. def bucket_quota_clear(self, bucket: str) -> str:
  659. """Clear bucket quota configuration."""
  660. return self.bucket_quota_set(bucket, 0)
  661. def bucket_quota_get(self, bucket: str) -> str:
  662. """Get bucket quota configuration."""
  663. response = self._url_open(
  664. method="GET",
  665. command=_COMMAND.GET_BUCKET_QUOTA,
  666. query_params={"bucket": bucket}
  667. )
  668. return response.data.decode()
  669. def get_data_usage_info(self):
  670. """Get data usage info"""
  671. response = self._url_open(
  672. method="GET",
  673. command=_COMMAND.DATA_USAGE_INFO,
  674. )
  675. return response.data.decode()
  676. def get_service_account(self, access_key: str) -> str:
  677. """Get information about service account"""
  678. response = self._url_open(
  679. method="GET",
  680. command=_COMMAND.SERVICE_ACCOUNT_INFO,
  681. query_params={"accessKey": access_key},
  682. preload_content=False,
  683. )
  684. plain_data = decrypt(
  685. response, self._provider.retrieve().secret_key,
  686. )
  687. return plain_data.decode()
  688. def list_service_account(self, user: str) -> str:
  689. """List service accounts of user"""
  690. response = self._url_open(
  691. method="GET",
  692. command=_COMMAND.SERVICE_ACCOUNT_LIST,
  693. query_params={"user": user},
  694. preload_content=False,
  695. )
  696. plain_data = decrypt(
  697. response, self._provider.retrieve().secret_key,
  698. )
  699. return plain_data.decode()
  700. def add_service_account(self,
  701. *,
  702. access_key: Optional[str] = None,
  703. secret_key: Optional[str] = None,
  704. name: Optional[str] = None,
  705. description: Optional[str] = None,
  706. policy: Optional[dict] = None,
  707. policy_file: Optional[str | os.PathLike] = None,
  708. expiration: Optional[str] = None,
  709. status: Optional[str] = None) -> str:
  710. """
  711. Add a new service account with the given access key and secret key
  712. """
  713. if (access_key is None) ^ (secret_key is None):
  714. raise ValueError("both access key and secret key must be provided")
  715. if access_key == "" or secret_key == "":
  716. raise ValueError("access key or secret key must not be empty")
  717. if policy_file is not None and policy is not None:
  718. raise ValueError("either policy_file or policy must be provided")
  719. data: dict[str, Any] = {
  720. "status": "enabled",
  721. "accessKey": access_key,
  722. "secretKey": secret_key,
  723. }
  724. if name:
  725. data["name"] = name
  726. if description:
  727. data["description"] = description
  728. if policy_file:
  729. with open(policy_file, encoding="utf-8") as file:
  730. data["policy"] = json.load(file)
  731. if policy:
  732. data["policy"] = policy
  733. if expiration:
  734. data["expiration"] = expiration
  735. if status:
  736. data["status"] = status
  737. body = json.dumps(data).encode()
  738. response = self._url_open(
  739. method="PUT",
  740. command=_COMMAND.SERVICE_ACCOUNT_ADD,
  741. body=encrypt(body, self._provider.retrieve().secret_key),
  742. preload_content=False,
  743. )
  744. plain_data = decrypt(
  745. response, self._provider.retrieve().secret_key,
  746. )
  747. return plain_data.decode()
  748. def update_service_account(self,
  749. *,
  750. access_key: str,
  751. secret_key: Optional[str] = None,
  752. name: Optional[str] = None,
  753. description: Optional[str] = None,
  754. policy_file: Optional[str | os.PathLike] = None,
  755. policy: Optional[dict] = None,
  756. expiration: Optional[str] = None,
  757. status: Optional[str] = None) -> str:
  758. """Update an existing service account"""
  759. args = [secret_key, name, description,
  760. policy_file, policy, expiration, status]
  761. if not any(arg for arg in args):
  762. raise ValueError("at least one of secret_key, name, description, "
  763. "policy_file, policy, expiration or status must "
  764. "be specified")
  765. if policy_file is not None and policy is not None:
  766. raise ValueError("either policy_file or policy must be provided")
  767. data: dict[str, Any] = {}
  768. if secret_key:
  769. data["newSecretKey"] = secret_key
  770. if name:
  771. data["newName"] = name
  772. if description:
  773. data["newDescription"] = description
  774. if policy_file:
  775. with open(policy_file, encoding="utf-8") as file:
  776. data["newPolicy"] = json.load(file)
  777. if policy:
  778. data["newPolicy"] = policy
  779. if expiration:
  780. data["newExpiration"] = expiration
  781. if status:
  782. data["newStatus"] = status
  783. body = json.dumps(data).encode()
  784. response = self._url_open(
  785. method="POST",
  786. command=_COMMAND.SERVICE_ACCOUNT_UPDATE,
  787. query_params={"accessKey": access_key},
  788. body=encrypt(body, self._provider.retrieve().secret_key),
  789. )
  790. return response.data.decode()
  791. def delete_service_account(self, access_key: str) -> str:
  792. """Delete a service account"""
  793. response = self._url_open(
  794. method="DELETE",
  795. command=_COMMAND.SERVICE_ACCOUNT_DELETE,
  796. query_params={"accessKey": access_key},
  797. )
  798. return response.data.decode()
  799. def _attach_detach_policy(
  800. self,
  801. command: _COMMAND,
  802. policies: list[str],
  803. user: Optional[str] = None,
  804. group: Optional[str] = None,
  805. ) -> str:
  806. """Attach or detach policies for builtin or LDAP."""
  807. if (user is not None) ^ (group is not None):
  808. key = "user" if user else "group"
  809. body = json.dumps(
  810. {"policies": policies,
  811. key: cast(str, user or group)},
  812. ).encode()
  813. response = self._url_open(
  814. method="POST",
  815. command=command,
  816. body=encrypt(body, self._provider.retrieve().secret_key),
  817. preload_content=False,
  818. )
  819. if (
  820. command in [
  821. _COMMAND.IDP_BUILTIN_POLICY_ATTACH,
  822. _COMMAND.IDP_BUILTIN_POLICY_DETACH,
  823. ] and
  824. response.status in [201, 204]
  825. ):
  826. # Older MinIO servers do not return response.
  827. response.close()
  828. response.release_conn()
  829. return ""
  830. data = decrypt(response, self._provider.retrieve().secret_key)
  831. return data.decode()
  832. raise ValueError("either user or group must be set")
  833. def attach_policy_ldap(
  834. self,
  835. policies: list[str],
  836. user: Optional[str] = None,
  837. group: Optional[str] = None,
  838. ) -> str:
  839. """Attach policies for LDAP."""
  840. return self._attach_detach_policy(
  841. _COMMAND.IDP_LDAP_POLICY_ATTACH, policies, user, group,
  842. )
  843. def detach_policy_ldap(
  844. self,
  845. policies: list[str],
  846. user: Optional[str] = None,
  847. group: Optional[str] = None,
  848. ) -> str:
  849. """Detach policies for LDAP."""
  850. return self._attach_detach_policy(
  851. _COMMAND.IDP_LDAP_POLICY_DETACH, policies, user, group,
  852. )
  853. def list_access_keys_ldap(
  854. self,
  855. user_dn: str,
  856. list_type: str,
  857. ) -> str:
  858. """List service accounts belonging to the specified user."""
  859. response = self._url_open(
  860. method="GET",
  861. command=_COMMAND.IDP_LDAP_LIST_ACCESS_KEYS,
  862. query_params={"userDN": user_dn, "listType": list_type},
  863. preload_content=False,
  864. )
  865. plain_data = decrypt(
  866. response, self._provider.retrieve().secret_key,
  867. )
  868. return plain_data.decode()
  869. def list_access_keys_ldap_bulk(
  870. self,
  871. users: list[str],
  872. list_type: str,
  873. all_users: bool,
  874. ) -> str:
  875. """List access keys belonging to the given users or all users."""
  876. if len(users) != 0 and all_users:
  877. raise ValueError("both users and all_users are not permitted")
  878. key, value = ("all", "true") if all_users else ("userDNs", users)
  879. response = self._url_open(
  880. method="GET",
  881. command=_COMMAND.IDP_LDAP_LIST_ACCESS_KEYS_BULK,
  882. query_params={"listType": list_type, key: value},
  883. preload_content=False,
  884. )
  885. plain_data = decrypt(
  886. response, self._provider.retrieve().secret_key,
  887. )
  888. return plain_data.decode()
  889. def attach_policy(
  890. self,
  891. policies: list[str],
  892. user: Optional[str] = None,
  893. group: Optional[str] = None,
  894. ) -> str:
  895. """Attach builtin policies."""
  896. return self._attach_detach_policy(
  897. _COMMAND.IDP_BUILTIN_POLICY_ATTACH, policies, user, group,
  898. )
  899. def detach_policy(
  900. self,
  901. policies: list[str],
  902. user: Optional[str] = None,
  903. group: Optional[str] = None,
  904. ) -> str:
  905. """Detach builtin policies."""
  906. return self._attach_detach_policy(
  907. _COMMAND.IDP_BUILTIN_POLICY_DETACH, policies, user, group,
  908. )
  909. def get_policy_entities(
  910. self,
  911. users: list[str],
  912. groups: list[str],
  913. policies: list[str],
  914. ) -> str:
  915. """Get builtin policy entities."""
  916. response = self._url_open(
  917. method="GET",
  918. command=_COMMAND.IDP_BUILTIN_POLICY_ENTITIES,
  919. query_params={"user": users, "group": groups, "policy": policies},
  920. preload_content=False,
  921. )
  922. plain_data = decrypt(
  923. response, self._provider.retrieve().secret_key,
  924. )
  925. return plain_data.decode()