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]