github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/cli/sawtooth_cli/identity.py (about) 1 # Copyright 2017 Intel Corporation 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 # ------------------------------------------------------------------------------ 15 16 from base64 import b64decode 17 import csv 18 import getpass 19 import hashlib 20 import json 21 import os 22 import sys 23 import time 24 import yaml 25 26 from sawtooth_cli.exceptions import CliException 27 from sawtooth_cli.rest_client import RestClient 28 from sawtooth_cli import tty 29 30 from sawtooth_cli.protobuf.identities_pb2 import IdentityPayload 31 from sawtooth_cli.protobuf.identity_pb2 import Policy 32 from sawtooth_cli.protobuf.identity_pb2 import PolicyList 33 from sawtooth_cli.protobuf.identity_pb2 import Role 34 from sawtooth_cli.protobuf.identity_pb2 import RoleList 35 from sawtooth_cli.protobuf.transaction_pb2 import TransactionHeader 36 from sawtooth_cli.protobuf.transaction_pb2 import Transaction 37 from sawtooth_cli.protobuf.batch_pb2 import BatchHeader 38 from sawtooth_cli.protobuf.batch_pb2 import Batch 39 from sawtooth_cli.protobuf.batch_pb2 import BatchList 40 from sawtooth_cli.sawset import setting_key_to_address 41 42 from sawtooth_signing import create_context 43 from sawtooth_signing import CryptoFactory 44 from sawtooth_signing import ParseError 45 from sawtooth_signing.secp256k1 import Secp256k1PrivateKey 46 47 48 IDENTITY_NAMESPACE = '00001d' 49 50 _MIN_PRINT_WIDTH = 15 51 _MAX_KEY_PARTS = 4 52 _FIRST_ADDRESS_PART_SIZE = 14 53 _ADDRESS_PART_SIZE = 16 54 _POLICY_PREFIX = "00" 55 _ROLE_PREFIX = "01" 56 _EMPTY_PART = hashlib.sha256("".encode()).hexdigest()[:_ADDRESS_PART_SIZE] 57 _REQUIRED_INPUT = setting_key_to_address("sawtooth.identity.allowed_keys") 58 59 60 def add_identity_parser(subparsers, parent_parser): 61 """Creates the arg parsers needed for the identity command and 62 its subcommands. 63 """ 64 # identity 65 parser = subparsers.add_parser( 66 'identity', 67 help='Works with optional roles, policies, and permissions', 68 description='Provides subcommands to work with roles and policies.') 69 70 identity_parsers = parser.add_subparsers( 71 title="subcommands", 72 dest="subcommand") 73 74 identity_parsers.required = True 75 76 # policy 77 policy_parser = identity_parsers.add_parser( 78 'policy', 79 help='Provides subcommands to display existing policies and create ' 80 'new policies', 81 description='Provides subcommands to list the current policies ' 82 'stored in state and to create new policies.') 83 84 policy_parsers = policy_parser.add_subparsers( 85 title='policy', 86 dest='policy_cmd') 87 88 policy_parsers.required = True 89 90 # policy create 91 create_parser = policy_parsers.add_parser( 92 'create', 93 help='Creates batches of sawtooth-identity transactions for setting a ' 94 'policy', 95 description='Creates a policy that can be set to a role or changes a ' 96 'policy without resetting the role.') 97 98 create_parser.add_argument( 99 '-k', '--key', 100 type=str, 101 help='specify the signing key for the resulting batches') 102 103 create_target_group = create_parser.add_mutually_exclusive_group() 104 105 create_target_group.add_argument( 106 '-o', '--output', 107 type=str, 108 help='specify the output filename for the resulting batches') 109 110 create_target_group.add_argument( 111 '--url', 112 type=str, 113 help="identify the URL of a validator's REST API", 114 default='http://localhost:8008') 115 116 create_parser.add_argument( 117 '--wait', 118 type=int, 119 default=15, 120 help="set time, in seconds, to wait for the policy to commit when " 121 "submitting to the REST API.") 122 123 create_parser.add_argument( 124 'name', 125 type=str, 126 help='name of the new policy') 127 128 create_parser.add_argument( 129 'rule', 130 type=str, 131 nargs="+", 132 help='rule with the format "PERMIT_KEY <key>" or "DENY_KEY <key> ' 133 '(multiple "rule" arguments can be specified)') 134 135 # policy list 136 list_parser = policy_parsers.add_parser( 137 'list', 138 help='Lists the current policies', 139 description='Lists the policies that are currently set in state.') 140 141 list_parser.add_argument( 142 '--url', 143 type=str, 144 help="identify the URL of a validator's REST API", 145 default='http://localhost:8008') 146 147 list_parser.add_argument( 148 '--format', 149 default='default', 150 choices=['default', 'csv', 'json', 'yaml'], 151 help='choose the output format') 152 153 # role 154 role_parser = identity_parsers.add_parser( 155 'role', 156 help='Provides subcommands to display existing roles and create ' 157 'new roles', 158 description='Provides subcommands to list the current roles ' 159 'stored in state and to create new roles.') 160 161 role_parsers = role_parser.add_subparsers( 162 title='role', 163 dest='role_cmd') 164 165 role_parsers.required = True 166 167 # role create 168 create_parser = role_parsers.add_parser( 169 'create', 170 help='Creates a new role that can be used to enforce permissions', 171 description='Creates a new role that can be used to enforce ' 172 'permissions.') 173 174 create_parser.add_argument( 175 '-k', '--key', 176 type=str, 177 help='specify the signing key for the resulting batches') 178 179 create_parser.add_argument( 180 '--wait', 181 type=int, 182 default=15, 183 help='set time, in seconds, to wait for a role to commit ' 184 'when submitting to the REST API.') 185 186 create_target_group = create_parser.add_mutually_exclusive_group() 187 188 create_target_group.add_argument( 189 '-o', '--output', 190 type=str, 191 help='specify the output filename for the resulting batches') 192 193 create_target_group.add_argument( 194 '--url', 195 type=str, 196 help="the URL of a validator's REST API", 197 default='http://localhost:8008') 198 199 create_parser.add_argument( 200 'name', 201 type=str, 202 help='name of the role') 203 204 create_parser.add_argument( 205 'policy', 206 type=str, 207 help='identify policy that role will be restricted to') 208 209 # role list 210 list_parser = role_parsers.add_parser( 211 'list', 212 help='Lists the current keys and values of roles', 213 description='Displays the roles that are currently set in state.') 214 215 list_parser.add_argument( 216 '--url', 217 type=str, 218 help="identify the URL of a validator's REST API", 219 default='http://localhost:8008') 220 221 list_parser.add_argument( 222 '--format', 223 default='default', 224 choices=['default', 'csv', 'json', 'yaml'], 225 help='choose the output format') 226 227 228 def do_identity(args): 229 """Executes the config commands subcommands. 230 """ 231 if args.subcommand == 'policy' and args.policy_cmd == 'create': 232 _do_identity_policy_create(args) 233 elif args.subcommand == 'policy' and args.policy_cmd == 'list': 234 _do_identity_policy_list(args) 235 elif args.subcommand == 'role' and args.role_cmd == 'create': 236 _do_identity_role_create(args) 237 elif args.subcommand == 'role' and args.role_cmd == 'list': 238 _do_identity_role_list(args) 239 else: 240 raise AssertionError( 241 '"{}" is not a valid subcommand of "identity"'.format( 242 args.subcommand)) 243 244 245 def _do_identity_policy_create(args): 246 """Executes the 'policy create' subcommand. Given a key file, and a 247 series of entries, it generates a batch of sawtooth_identity 248 transactions in a BatchList instance. The BatchList is either stored to a 249 file or submitted to a validator, depending on the supplied CLI arguments. 250 """ 251 signer = _read_signer(args.key) 252 253 txns = [_create_policy_txn(signer, args.name, args.rule)] 254 255 batch = _create_batch(signer, txns) 256 257 batch_list = BatchList(batches=[batch]) 258 259 if args.output is not None: 260 try: 261 with open(args.output, 'wb') as batch_file: 262 batch_file.write(batch_list.SerializeToString()) 263 except IOError as e: 264 raise CliException( 265 'Unable to write to batch file: {}'.format(str(e))) 266 elif args.url is not None: 267 rest_client = RestClient(args.url) 268 rest_client.send_batches(batch_list) 269 if args.wait and args.wait > 0: 270 batch_id = batch.header_signature 271 wait_time = 0 272 start_time = time.time() 273 274 while wait_time < args.wait: 275 statuses = rest_client.get_statuses( 276 [batch_id], 277 args.wait - int(wait_time)) 278 wait_time = time.time() - start_time 279 if statuses[0]['status'] == 'COMMITTED': 280 print( 281 'Policy committed in {:.6} sec'.format(wait_time)) 282 return 283 284 # Wait a moment so as not to hammer the Rest Api 285 time.sleep(0.2) 286 287 print('Wait timed out! Policy was not committed...') 288 print('{:128.128} {}'.format( 289 batch_id, 290 statuses[0]['status'])) 291 exit(1) 292 else: 293 raise AssertionError('No target for create set.') 294 295 296 def _do_identity_policy_list(args): 297 rest_client = RestClient(args.url) 298 state = rest_client.list_state(subtree=IDENTITY_NAMESPACE + _POLICY_PREFIX) 299 300 head = state['head'] 301 state_values = state['data'] 302 printable_policies = [] 303 for state_value in state_values: 304 policies_list = PolicyList() 305 decoded = b64decode(state_value['data']) 306 policies_list.ParseFromString(decoded) 307 308 for policy in policies_list.policies: 309 printable_policies.append(policy) 310 311 printable_policies.sort(key=lambda p: p.name) 312 313 if args.format == 'default': 314 tty_width = tty.width() 315 for policy in printable_policies: 316 # Set value width to the available terminal space, or the min width 317 width = tty_width - len(policy.name) - 3 318 width = width if width > _MIN_PRINT_WIDTH else _MIN_PRINT_WIDTH 319 value = "Entries:\n" 320 for entry in policy.entries: 321 entry_string = (" " * 4) + Policy.EntryType.Name(entry.type) \ 322 + " " + entry.key 323 value += (entry_string[:width] + '...' 324 if len(entry_string) > width 325 else entry_string) + "\n" 326 print('{}: \n {}'.format(policy.name, value)) 327 elif args.format == 'csv': 328 try: 329 writer = csv.writer(sys.stdout, quoting=csv.QUOTE_ALL) 330 writer.writerow(['POLICY NAME', 'ENTRIES']) 331 for policy in printable_policies: 332 output = [policy.name] 333 for entry in policy.entries: 334 output.append( 335 Policy.EntryType.Name(entry.type) + " " + entry.key) 336 writer.writerow(output) 337 except csv.Error: 338 raise CliException('Error writing CSV') 339 elif args.format == 'json' or args.format == 'yaml': 340 output = {} 341 for policy in printable_policies: 342 value = "Entries: " 343 for entry in policy.entries: 344 entry_string = Policy.EntryType.Name(entry.type) + " " \ 345 + entry.key 346 value += entry_string + " " 347 output[policy.name] = value 348 349 policies_snapshot = { 350 'head': head, 351 'policies': output 352 } 353 if args.format == 'json': 354 print(json.dumps(policies_snapshot, indent=2, sort_keys=True)) 355 else: 356 print(yaml.dump(policies_snapshot, default_flow_style=False)[0:-1]) 357 else: 358 raise AssertionError('Unknown format {}'.format(args.format)) 359 360 361 def _do_identity_role_create(args): 362 """Executes the 'role create' subcommand. Given a key file, a role name, 363 and a policy name it generates a batch of sawtooth_identity 364 transactions in a BatchList instance. The BatchList is either stored to a 365 file or submitted to a validator, depending on the supplied CLI arguments. 366 """ 367 signer = _read_signer(args.key) 368 txns = [_create_role_txn(signer, args.name, 369 args.policy)] 370 371 batch = _create_batch(signer, txns) 372 373 batch_list = BatchList(batches=[batch]) 374 375 if args.output is not None: 376 try: 377 with open(args.output, 'wb') as batch_file: 378 batch_file.write(batch_list.SerializeToString()) 379 except IOError as e: 380 raise CliException( 381 'Unable to write to batch file: {}'.format(str(e))) 382 elif args.url is not None: 383 rest_client = RestClient(args.url) 384 rest_client.send_batches(batch_list) 385 if args.wait and args.wait > 0: 386 batch_id = batch.header_signature 387 wait_time = 0 388 start_time = time.time() 389 390 while wait_time < args.wait: 391 statuses = rest_client.get_statuses( 392 [batch_id], 393 args.wait - int(wait_time)) 394 wait_time = time.time() - start_time 395 396 if statuses[0]['status'] == 'COMMITTED': 397 print( 398 'Role committed in {:.6} sec'.format(wait_time)) 399 return 400 401 # Wait a moment so as not to hammer the Rest Api 402 time.sleep(0.2) 403 404 print('Wait timed out! Role was not committed...') 405 print('{:128.128} {}'.format( 406 batch_id, 407 statuses[0]['status'])) 408 exit(1) 409 else: 410 raise AssertionError('No target for create set.') 411 412 413 def _do_identity_role_list(args): 414 """Lists the current on-chain configuration values. 415 """ 416 rest_client = RestClient(args.url) 417 state = rest_client.list_state(subtree=IDENTITY_NAMESPACE + _ROLE_PREFIX) 418 419 head = state['head'] 420 state_values = state['data'] 421 printable_roles = [] 422 for state_value in state_values: 423 role_list = RoleList() 424 decoded = b64decode(state_value['data']) 425 role_list.ParseFromString(decoded) 426 427 for role in role_list.roles: 428 printable_roles.append(role) 429 430 printable_roles.sort(key=lambda r: r.name) 431 432 if args.format == 'default': 433 tty_width = tty.width() 434 for role in printable_roles: 435 # Set value width to the available terminal space, or the min width 436 width = tty_width - len(role.name) - 3 437 width = width if width > _MIN_PRINT_WIDTH else _MIN_PRINT_WIDTH 438 value = (role.policy_name[:width] + '...' 439 if len(role.policy_name) > width 440 else role.policy_name) 441 print('{}: {}'.format(role.name, value)) 442 elif args.format == 'csv': 443 try: 444 writer = csv.writer(sys.stdout, quoting=csv.QUOTE_ALL) 445 writer.writerow(['KEY', 'VALUE']) 446 for role in printable_roles: 447 writer.writerow([role.name, role.policy_name]) 448 except csv.Error: 449 raise CliException('Error writing CSV') 450 elif args.format == 'json' or args.format == 'yaml': 451 roles_snapshot = { 452 'head': head, 453 'roles': {role.name: role.policy_name 454 for role in printable_roles} 455 } 456 if args.format == 'json': 457 print(json.dumps(roles_snapshot, indent=2, sort_keys=True)) 458 else: 459 print(yaml.dump(roles_snapshot, default_flow_style=False)[0:-1]) 460 else: 461 raise AssertionError('Unknown format {}'.format(args.format)) 462 463 464 def _create_policy_txn(signer, policy_name, rules): 465 entries = [] 466 for rule in rules: 467 rule = rule.split(" ") 468 if rule[0] == "PERMIT_KEY": 469 entry = Policy.Entry(type=Policy.PERMIT_KEY, 470 key=rule[1]) 471 entries.append(entry) 472 elif rule[0] == "DENY_KEY": 473 entry = Policy.Entry(type=Policy.DENY_KEY, 474 key=rule[1]) 475 entries.append(entry) 476 policy = Policy(name=policy_name, entries=entries) 477 payload = IdentityPayload(type=IdentityPayload.POLICY, 478 data=policy.SerializeToString()) 479 480 policy_address = _policy_to_address(policy_name) 481 482 header = TransactionHeader( 483 signer_public_key=signer.get_public_key().as_hex(), 484 family_name='sawtooth_identity', 485 family_version='1.0', 486 inputs=[_REQUIRED_INPUT, policy_address], 487 outputs=[policy_address], 488 dependencies=[], 489 payload_sha512=hashlib.sha512( 490 payload.SerializeToString()).hexdigest(), 491 batcher_public_key=signer.get_public_key().as_hex(), 492 nonce=time.time().hex().encode()) 493 494 header_bytes = header.SerializeToString() 495 496 transaction = Transaction( 497 header=header_bytes, 498 payload=payload.SerializeToString(), 499 header_signature=signer.sign(header_bytes)) 500 501 return transaction 502 503 504 def _create_role_txn(signer, role_name, policy_name): 505 role = Role(name=role_name, policy_name=policy_name) 506 payload = IdentityPayload(type=IdentityPayload.ROLE, 507 data=role.SerializeToString()) 508 509 policy_address = _policy_to_address(policy_name) 510 role_address = _role_to_address(role_name) 511 512 header = TransactionHeader( 513 signer_public_key=signer.get_public_key().as_hex(), 514 family_name='sawtooth_identity', 515 family_version='1.0', 516 inputs=[_REQUIRED_INPUT, policy_address, role_address], 517 outputs=[role_address], 518 dependencies=[], 519 payload_sha512=hashlib.sha512( 520 payload.SerializeToString()).hexdigest(), 521 batcher_public_key=signer.get_public_key().as_hex(), 522 nonce=time.time().hex().encode()) 523 524 header_bytes = header.SerializeToString() 525 526 transaction = Transaction( 527 header=header_bytes, 528 payload=payload.SerializeToString(), 529 header_signature=signer.sign(header_bytes)) 530 531 return transaction 532 533 534 def _read_signer(key_filename): 535 """Reads the given file as a hex key. 536 537 Args: 538 key_filename: The filename where the key is stored. If None, 539 defaults to the default key for the current user. 540 541 Returns: 542 Signer: the signer 543 544 Raises: 545 CliException: If unable to read the file. 546 """ 547 filename = key_filename 548 if filename is None: 549 filename = os.path.join(os.path.expanduser('~'), 550 '.sawtooth', 551 'keys', 552 getpass.getuser() + '.priv') 553 554 try: 555 with open(filename, 'r') as key_file: 556 signing_key = key_file.read().strip() 557 except IOError as e: 558 raise CliException('Unable to read key file: {}'.format(str(e))) 559 560 try: 561 private_key = Secp256k1PrivateKey.from_hex(signing_key) 562 except ParseError as e: 563 raise CliException('Unable to read key in file: {}'.format(str(e))) 564 565 context = create_context('secp256k1') 566 crypto_factory = CryptoFactory(context) 567 return crypto_factory.new_signer(private_key) 568 569 570 def _create_batch(signer, transactions): 571 """Creates a batch from a list of transactions and a public key, and signs 572 the resulting batch with the given signing key. 573 574 Args: 575 signer (:obj:`Signer`): The cryptographic signer 576 transactions (list of `Transaction`): The transactions to add to the 577 batch. 578 579 Returns: 580 `Batch`: The constructed and signed batch. 581 """ 582 txn_ids = [txn.header_signature for txn in transactions] 583 batch_header = BatchHeader( 584 signer_public_key=signer.get_public_key().as_hex(), 585 transaction_ids=txn_ids).SerializeToString() 586 587 return Batch( 588 header=batch_header, 589 header_signature=signer.sign(batch_header), 590 transactions=transactions) 591 592 593 def _to_hash(value): 594 return hashlib.sha256(value.encode()).hexdigest() 595 596 597 def _role_to_address(role_name): 598 # split the key into 4 parts, maximum 599 key_parts = role_name.split('.', maxsplit=_MAX_KEY_PARTS - 1) 600 601 # compute the short hash of each part 602 addr_parts = [_to_hash(key_parts[0])[:_FIRST_ADDRESS_PART_SIZE]] 603 addr_parts += [_to_hash(x)[:_ADDRESS_PART_SIZE] for x in 604 key_parts[1:]] 605 # pad the parts with the empty hash, if needed 606 addr_parts.extend([_EMPTY_PART] * (_MAX_KEY_PARTS - len(addr_parts))) 607 return IDENTITY_NAMESPACE + _ROLE_PREFIX + ''.join(addr_parts) 608 609 610 def _policy_to_address(policy_name): 611 return IDENTITY_NAMESPACE + _POLICY_PREFIX + \ 612 _to_hash(policy_name)[:62]