[Notes] [Git][BuildGrid/buildgrid][mablanch/144-jwt-authentication] 13 commits: setup.py: Introduce pyjwt dependency



Title: GitLab

Martin Blanchard pushed to branch mablanch/144-jwt-authentication at BuildGrid / buildgrid

Commits:

28 changed files:

Changes:

  • .gitlab-ci.yml
    ... ... @@ -20,6 +20,7 @@ before_script:
    20 20
       variables:
    
    21 21
         PYTEST_ADDOPTS: "--color=yes"
    
    22 22
       script:
    
    23
    +    - python3 -m pip install --user --editable ".[auth]"
    
    23 24
         - python3 setup.py test
    
    24 25
         - mkdir -p coverage/
    
    25 26
         - cp .coverage coverage/coverage."${CI_JOB_NAME}"
    

  • .pylintrc
    ... ... @@ -186,7 +186,8 @@ ignore-on-opaque-inference=yes
    186 186
     # qualified names.
    
    187 187
     ignored-classes=google.protobuf.any_pb2.Any,
    
    188 188
                     google.protobuf.duration_pb2.Duration,
    
    189
    -                google.protobuf.timestamp_pb2.Timestamp
    
    189
    +                google.protobuf.timestamp_pb2.Timestamp,
    
    190
    +                jwt
    
    190 191
     
    
    191 192
     # List of module names for which member attributes should not be checked
    
    192 193
     # (useful for modules/projects where namespaces are manipulated during runtime
    
    ... ... @@ -462,6 +463,7 @@ known-third-party=boto3,
    462 463
                       google,
    
    463 464
                       grpc,
    
    464 465
                       janus,
    
    466
    +                  jwt,
    
    465 467
                       moto,
    
    466 468
                       yaml
    
    467 469
     
    
    ... ... @@ -525,4 +527,4 @@ valid-metaclass-classmethod-first-arg=mcs
    525 527
     
    
    526 528
     # Exceptions that will emit a warning when being caught. Defaults to
    
    527 529
     # "Exception"
    
    528
    -overgeneral-exceptions=Exception
    530
    +overgeneral-exceptions=Exception
    \ No newline at end of file

  • buildgrid/_app/commands/cmd_bot.py
    ... ... @@ -22,16 +22,15 @@ Create a bot interface and request work
    22 22
     
    
    23 23
     from pathlib import Path, PurePath
    
    24 24
     import sys
    
    25
    -from urllib.parse import urlparse
    
    26 25
     
    
    27 26
     import click
    
    28
    -import grpc
    
    29 27
     
    
    30 28
     from buildgrid.bot import bot, interface, session
    
    31 29
     from buildgrid.bot.hardware.interface import HardwareInterface
    
    32 30
     from buildgrid.bot.hardware.device import Device
    
    33 31
     from buildgrid.bot.hardware.worker import Worker
    
    34
    -
    
    32
    +from buildgrid.client.authentication import setup_channel
    
    33
    +from buildgrid._exceptions import InvalidArgumentError
    
    35 34
     
    
    36 35
     from ..bots import buildbox, dummy, host
    
    37 36
     from ..cli import pass_context, setup_logging
    
    ... ... @@ -40,20 +39,22 @@ from ..cli import pass_context, setup_logging
    40 39
     @click.group(name='bot', short_help="Create and register bot clients.")
    
    41 40
     @click.option('--remote', type=click.STRING, default='http://localhost:50051', show_default=True,
    
    42 41
                   help="Remote execution server's URL (port defaults to 50051 if not specified).")
    
    42
    +@click.option('--auth-token', type=click.Path(exists=True, dir_okay=False), default=None,
    
    43
    +              help="Authorization token for the remote.")
    
    43 44
     @click.option('--client-key', type=click.Path(exists=True, dir_okay=False), default=None,
    
    44
    -              help="Private client key for TLS (PEM-encoded)")
    
    45
    +              help="Private client key for TLS (PEM-encoded).")
    
    45 46
     @click.option('--client-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    46
    -              help="Public client certificate for TLS (PEM-encoded)")
    
    47
    +              help="Public client certificate for TLS (PEM-encoded).")
    
    47 48
     @click.option('--server-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    48
    -              help="Public server certificate for TLS (PEM-encoded)")
    
    49
    +              help="Public server certificate for TLS (PEM-encoded).")
    
    49 50
     @click.option('--remote-cas', type=click.STRING, default=None, show_default=True,
    
    50 51
                   help="Remote CAS server's URL (port defaults to 11001 if not specified).")
    
    51 52
     @click.option('--cas-client-key', type=click.Path(exists=True, dir_okay=False), default=None,
    
    52
    -              help="Private CAS client key for TLS (PEM-encoded)")
    
    53
    +              help="Private CAS client key for TLS (PEM-encoded).")
    
    53 54
     @click.option('--cas-client-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    54
    -              help="Public CAS client certificate for TLS (PEM-encoded)")
    
    55
    +              help="Public CAS client certificate for TLS (PEM-encoded).")
    
    55 56
     @click.option('--cas-server-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    56
    -              help="Public CAS server certificate for TLS (PEM-encoded)")
    
    57
    +              help="Public CAS server certificate for TLS (PEM-encoded).")
    
    57 58
     @click.option('--update-period', type=click.FLOAT, default=0.5, show_default=True,
    
    58 59
                   help="Time period for bot updates to the server in seconds.")
    
    59 60
     @click.option('--parent', type=click.STRING, default='main', show_default=True,
    
    ... ... @@ -61,69 +62,31 @@ from ..cli import pass_context, setup_logging
    61 62
     @click.option('-v', '--verbose', count=True,
    
    62 63
                   help='Increase log verbosity level.')
    
    63 64
     @pass_context
    
    64
    -def cli(context, parent, update_period, remote, client_key, client_cert, server_cert,
    
    65
    +def cli(context, parent, update_period, remote, auth_token, client_key, client_cert, server_cert,
    
    65 66
             remote_cas, cas_client_key, cas_client_cert, cas_server_cert, verbose):
    
    66 67
         setup_logging(verbosity=verbose)
    
    67 68
         # Setup the remote execution server channel:
    
    68
    -    url = urlparse(remote)
    
    69
    -
    
    70
    -    context.remote = '{}:{}'.format(url.hostname, url.port or 50051)
    
    71
    -    context.remote_url = remote
    
    72
    -    context.update_period = update_period
    
    73
    -    context.parent = parent
    
    74
    -
    
    75
    -    if url.scheme == 'http':
    
    76
    -        context.channel = grpc.insecure_channel(context.remote)
    
    77
    -
    
    78
    -        context.client_key = None
    
    79
    -        context.client_cert = None
    
    80
    -        context.server_cert = None
    
    81
    -    else:
    
    82
    -        credentials = context.load_client_credentials(client_key, client_cert, server_cert)
    
    83
    -        if not credentials:
    
    84
    -            click.echo("ERROR: no TLS keys were specified and no defaults could be found.", err=True)
    
    85
    -            sys.exit(-1)
    
    86
    -
    
    87
    -        context.channel = grpc.secure_channel(context.remote, credentials)
    
    88
    -
    
    89
    -        context.client_key = credentials.client_key
    
    90
    -        context.client_cert = credentials.client_cert
    
    91
    -        context.server_cert = credentials.server_cert
    
    92
    -
    
    93
    -    # Setup the remote CAS server channel, if separated:
    
    94
    -    if remote_cas is not None and remote_cas != remote:
    
    95
    -        cas_url = urlparse(remote_cas)
    
    96
    -
    
    97
    -        context.remote_cas = '{}:{}'.format(cas_url.hostname, cas_url.port or 11001)
    
    98
    -        context.remote_cas_url = remote_cas
    
    69
    +    try:
    
    70
    +        context.channel, details = setup_channel(remote, auth_token=auth_token, server_cert=server_cert,
    
    71
    +                                                 client_key=client_key, client_cert=client_cert)
    
    99 72
     
    
    100
    -        if cas_url.scheme == 'http':
    
    101
    -            context.cas_channel = grpc.insecure_channel(context.remote_cas)
    
    73
    +        if remote_cas is not None and remote_cas != remote:
    
    74
    +            context.cas_channel, details = setup_channel(remote_cas, server_cert=cas_server_cert,
    
    75
    +                                                         client_key=cas_client_key, client_cert=cas_client_cert)
    
    76
    +            context.remote_cas_url = remote_cas
    
    102 77
     
    
    103
    -            context.cas_client_key = None
    
    104
    -            context.cas_client_cert = None
    
    105
    -            context.cas_server_cert = None
    
    106 78
             else:
    
    107
    -            cas_credentials = context.load_client_credentials(cas_client_key, cas_client_cert, cas_server_cert)
    
    108
    -            if not cas_credentials:
    
    109
    -                click.echo("ERROR: no TLS keys were specified and no defaults could be found.", err=True)
    
    110
    -                sys.exit(-1)
    
    111
    -
    
    112
    -            context.cas_channel = grpc.secure_channel(context.remote_cas, cas_credentials)
    
    79
    +            context.cas_channel = context.channel
    
    80
    +            context.remote_cas_url = remote
    
    113 81
     
    
    114
    -            context.cas_client_key = cas_credentials.client_key
    
    115
    -            context.cas_client_cert = cas_credentials.client_cert
    
    116
    -            context.cas_server_cert = cas_credentials.server_cert
    
    82
    +        context.cas_client_key, context.cas_client_cert, context.cas_server_cert = details
    
    117 83
     
    
    118
    -    else:
    
    119
    -        context.remote_cas = context.remote
    
    120
    -        context.remote_cas_url = remote
    
    84
    +    except InvalidArgumentError as e:
    
    85
    +        click.echo("Error: {}.".format(e), err=True)
    
    86
    +        sys.exit(-1)
    
    121 87
     
    
    122
    -        context.cas_channel = context.channel
    
    123
    -
    
    124
    -        context.cas_client_key = context.client_key
    
    125
    -        context.cas_client_cert = context.client_cert
    
    126
    -        context.cas_server_cert = context.server_cert
    
    88
    +    context.update_period = update_period
    
    89
    +    context.parent = parent
    
    127 90
     
    
    128 91
         bot_interface = interface.BotInterface(context.channel)
    
    129 92
     
    

  • buildgrid/_app/commands/cmd_capabilities.py
    ... ... @@ -13,13 +13,12 @@
    13 13
     # limitations under the License.
    
    14 14
     
    
    15 15
     
    
    16
    -import sys
    
    17
    -from urllib.parse import urlparse
    
    18
    -
    
    19 16
     import click
    
    20
    -import grpc
    
    17
    +from google.protobuf import json_format
    
    21 18
     
    
    19
    +from buildgrid.client.authentication import setup_channel
    
    22 20
     from buildgrid.client.capabilities import CapabilitiesInterface
    
    21
    +from buildgrid._exceptions import InvalidArgumentError
    
    23 22
     
    
    24 23
     from ..cli import pass_context
    
    25 24
     
    
    ... ... @@ -27,32 +26,29 @@ from ..cli import pass_context
    27 26
     @click.command(name='capabilities', short_help="Capabilities service.")
    
    28 27
     @click.option('--remote', type=click.STRING, default='http://localhost:50051', show_default=True,
    
    29 28
                   help="Remote execution server's URL (port defaults to 50051 if no specified).")
    
    29
    +@click.option('--auth-token', type=click.Path(exists=True, dir_okay=False), default=None,
    
    30
    +              help="Authorization token for the remote.")
    
    30 31
     @click.option('--client-key', type=click.Path(exists=True, dir_okay=False), default=None,
    
    31
    -              help="Private client key for TLS (PEM-encoded)")
    
    32
    +              help="Private client key for TLS (PEM-encoded).")
    
    32 33
     @click.option('--client-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    33
    -              help="Public client certificate for TLS (PEM-encoded)")
    
    34
    +              help="Public client certificate for TLS (PEM-encoded).")
    
    34 35
     @click.option('--server-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    35
    -              help="Public server certificate for TLS (PEM-encoded)")
    
    36
    +              help="Public server certificate for TLS (PEM-encoded).")
    
    36 37
     @click.option('--instance-name', type=click.STRING, default='main', show_default=True,
    
    37 38
                   help="Targeted farm instance name.")
    
    38 39
     @pass_context
    
    39
    -def cli(context, remote, instance_name, client_key, client_cert, server_cert):
    
    40
    -    click.echo("Getting capabilities...")
    
    41
    -    url = urlparse(remote)
    
    42
    -
    
    43
    -    remote = '{}:{}'.format(url.hostname, url.port or 50051)
    
    44
    -    instance_name = instance_name
    
    45
    -
    
    46
    -    if url.scheme == 'http':
    
    47
    -        channel = grpc.insecure_channel(remote)
    
    48
    -    else:
    
    49
    -        credentials = context.load_client_credentials(client_key, client_cert, server_cert)
    
    50
    -        if not credentials:
    
    51
    -            click.echo("ERROR: no TLS keys were specified and no defaults could be found.", err=True)
    
    52
    -            sys.exit(-1)
    
    53
    -
    
    54
    -        channel = grpc.secure_channel(remote, credentials)
    
    55
    -
    
    56
    -    interface = CapabilitiesInterface(channel)
    
    57
    -    response = interface.get_capabilities(instance_name)
    
    58
    -    click.echo(response)
    40
    +def cli(context, remote, instance_name, auth_token, client_key, client_cert, server_cert):
    
    41
    +    """Entry point for the bgd-capabilities CLI command group."""
    
    42
    +    try:
    
    43
    +        context.channel, _ = setup_channel(remote, auth_token=auth_token,
    
    44
    +                                           client_key=client_key, client_cert=client_cert)
    
    45
    +
    
    46
    +    except InvalidArgumentError as e:
    
    47
    +        click.echo("Error: {}.".format(e), err=True)
    
    48
    +
    
    49
    +    context.instance_name = instance_name
    
    50
    +
    
    51
    +    interface = CapabilitiesInterface(context.channel)
    
    52
    +    response = interface.get_capabilities(context.instance_name)
    
    53
    +
    
    54
    +    click.echo(json_format.MessageToJson(response))

  • buildgrid/_app/commands/cmd_cas.py
    ... ... @@ -21,13 +21,12 @@ Request work to be executed and monitor status of jobs.
    21 21
     """
    
    22 22
     
    
    23 23
     import os
    
    24
    -import sys
    
    25
    -from urllib.parse import urlparse
    
    26 24
     
    
    27 25
     import click
    
    28
    -import grpc
    
    29 26
     
    
    27
    +from buildgrid.client.authentication import setup_channel
    
    30 28
     from buildgrid.client.cas import download, upload
    
    29
    +from buildgrid._exceptions import InvalidArgumentError
    
    31 30
     from buildgrid._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
    
    32 31
     from buildgrid.utils import create_digest, merkle_tree_maker, read_file
    
    33 32
     
    
    ... ... @@ -37,32 +36,27 @@ from ..cli import pass_context
    37 36
     @click.group(name='cas', short_help="Interact with the CAS server.")
    
    38 37
     @click.option('--remote', type=click.STRING, default='http://localhost:50051', show_default=True,
    
    39 38
                   help="Remote execution server's URL (port defaults to 50051 if no specified).")
    
    39
    +@click.option('--auth-token', type=click.Path(exists=True, dir_okay=False), default=None,
    
    40
    +              help="Authorization token for the remote.")
    
    40 41
     @click.option('--client-key', type=click.Path(exists=True, dir_okay=False), default=None,
    
    41
    -              help="Private client key for TLS (PEM-encoded)")
    
    42
    +              help="Private client key for TLS (PEM-encoded).")
    
    42 43
     @click.option('--client-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    43
    -              help="Public client certificate for TLS (PEM-encoded)")
    
    44
    +              help="Public client certificate for TLS (PEM-encoded).")
    
    44 45
     @click.option('--server-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    45 46
                   help="Public server certificate for TLS (PEM-encoded)")
    
    46 47
     @click.option('--instance-name', type=click.STRING, default='main', show_default=True,
    
    47 48
                   help="Targeted farm instance name.")
    
    48 49
     @pass_context
    
    49
    -def cli(context, remote, instance_name, client_key, client_cert, server_cert):
    
    50
    -    url = urlparse(remote)
    
    50
    +def cli(context, remote, instance_name, auth_token, client_key, client_cert, server_cert):
    
    51
    +    """Entry point for the bgd-cas CLI command group."""
    
    52
    +    try:
    
    53
    +        context.channel, _ = setup_channel(remote, auth_token=auth_token,
    
    54
    +                                           client_key=client_key, client_cert=client_cert)
    
    51 55
     
    
    52
    -    context.remote = '{}:{}'.format(url.hostname, url.port or 50051)
    
    53
    -    context.instance_name = instance_name
    
    54
    -
    
    55
    -    if url.scheme == 'http':
    
    56
    -        context.channel = grpc.insecure_channel(context.remote)
    
    57
    -    else:
    
    58
    -        credentials = context.load_client_credentials(client_key, client_cert, server_cert)
    
    59
    -        if not credentials:
    
    60
    -            click.echo("ERROR: no TLS keys were specified and no defaults could be found.", err=True)
    
    61
    -            sys.exit(-1)
    
    56
    +    except InvalidArgumentError as e:
    
    57
    +        click.echo("Error: {}.".format(e), err=True)
    
    62 58
     
    
    63
    -        context.channel = grpc.secure_channel(context.remote, credentials)
    
    64
    -
    
    65
    -    click.echo("Starting for remote=[{}]".format(context.remote))
    
    59
    +    context.instance_name = instance_name
    
    66 60
     
    
    67 61
     
    
    68 62
     @cli.command('upload-dummy', short_help="Upload a dummy action. Should be used with `execute dummy-request`")
    

  • buildgrid/_app/commands/cmd_execute.py
    ... ... @@ -23,12 +23,12 @@ Request work to be executed and monitor status of jobs.
    23 23
     import os
    
    24 24
     import stat
    
    25 25
     import sys
    
    26
    -from urllib.parse import urlparse
    
    27 26
     
    
    28 27
     import click
    
    29
    -import grpc
    
    30 28
     
    
    29
    +from buildgrid.client.authentication import setup_channel
    
    31 30
     from buildgrid.client.cas import download, upload
    
    31
    +from buildgrid._exceptions import InvalidArgumentError
    
    32 32
     from buildgrid._protos.build.bazel.remote.execution.v2 import remote_execution_pb2, remote_execution_pb2_grpc
    
    33 33
     from buildgrid.utils import create_digest
    
    34 34
     
    
    ... ... @@ -38,32 +38,27 @@ from ..cli import pass_context
    38 38
     @click.group(name='execute', short_help="Execute simple operations.")
    
    39 39
     @click.option('--remote', type=click.STRING, default='http://localhost:50051', show_default=True,
    
    40 40
                   help="Remote execution server's URL (port defaults to 50051 if no specified).")
    
    41
    +@click.option('--auth-token', type=click.Path(exists=True, dir_okay=False), default=None,
    
    42
    +              help="Authorization token for the remote.")
    
    41 43
     @click.option('--client-key', type=click.Path(exists=True, dir_okay=False), default=None,
    
    42
    -              help="Private client key for TLS (PEM-encoded)")
    
    44
    +              help="Private client key for TLS (PEM-encoded).")
    
    43 45
     @click.option('--client-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    44
    -              help="Public client certificate for TLS (PEM-encoded)")
    
    46
    +              help="Public client certificate for TLS (PEM-encoded).")
    
    45 47
     @click.option('--server-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    46
    -              help="Public server certificate for TLS (PEM-encoded)")
    
    48
    +              help="Public server certificate for TLS (PEM-encoded).")
    
    47 49
     @click.option('--instance-name', type=click.STRING, default='main', show_default=True,
    
    48 50
                   help="Targeted farm instance name.")
    
    49 51
     @pass_context
    
    50
    -def cli(context, remote, instance_name, client_key, client_cert, server_cert):
    
    51
    -    url = urlparse(remote)
    
    52
    +def cli(context, remote, instance_name, auth_token, client_key, client_cert, server_cert):
    
    53
    +    """Entry point for the bgd-execute CLI command group."""
    
    54
    +    try:
    
    55
    +        context.channel, _ = setup_channel(remote, auth_token=auth_token,
    
    56
    +                                           client_key=client_key, client_cert=client_cert)
    
    52 57
     
    
    53
    -    context.remote = '{}:{}'.format(url.hostname, url.port or 50051)
    
    54
    -    context.instance_name = instance_name
    
    55
    -
    
    56
    -    if url.scheme == 'http':
    
    57
    -        context.channel = grpc.insecure_channel(context.remote)
    
    58
    -    else:
    
    59
    -        credentials = context.load_client_credentials(client_key, client_cert, server_cert)
    
    60
    -        if not credentials:
    
    61
    -            click.echo("ERROR: no TLS keys were specified and no defaults could be found.", err=True)
    
    62
    -            sys.exit(-1)
    
    58
    +    except InvalidArgumentError as e:
    
    59
    +        click.echo("Error: {}.".format(e), err=True)
    
    63 60
     
    
    64
    -        context.channel = grpc.secure_channel(context.remote, credentials)
    
    65
    -
    
    66
    -    click.echo("Starting for remote=[{}]".format(context.remote))
    
    61
    +    context.instance_name = instance_name
    
    67 62
     
    
    68 63
     
    
    69 64
     @cli.command('request-dummy', short_help="Send a dummy action.")
    

  • buildgrid/_app/commands/cmd_operation.py
    ... ... @@ -22,15 +22,14 @@ Check the status of operations
    22 22
     
    
    23 23
     from collections import OrderedDict
    
    24 24
     from operator import attrgetter
    
    25
    -from urllib.parse import urlparse
    
    26
    -import sys
    
    27 25
     from textwrap import indent
    
    28 26
     
    
    29 27
     import click
    
    30 28
     from google.protobuf import json_format
    
    31
    -import grpc
    
    32 29
     
    
    30
    +from buildgrid.client.authentication import setup_channel
    
    33 31
     from buildgrid._enums import OperationStage
    
    32
    +from buildgrid._exceptions import InvalidArgumentError
    
    34 33
     from buildgrid._protos.build.bazel.remote.execution.v2 import remote_execution_pb2, remote_execution_pb2_grpc
    
    35 34
     from buildgrid._protos.google.longrunning import operations_pb2, operations_pb2_grpc
    
    36 35
     from buildgrid._protos.google.rpc import code_pb2
    
    ... ... @@ -41,32 +40,27 @@ from ..cli import pass_context
    41 40
     @click.group(name='operation', short_help="Long running operations commands.")
    
    42 41
     @click.option('--remote', type=click.STRING, default='http://localhost:50051', show_default=True,
    
    43 42
                   help="Remote execution server's URL (port defaults to 50051 if no specified).")
    
    43
    +@click.option('--auth-token', type=click.Path(exists=True, dir_okay=False), default=None,
    
    44
    +              help="Authorization token for the remote.")
    
    44 45
     @click.option('--client-key', type=click.Path(exists=True, dir_okay=False), default=None,
    
    45
    -              help="Private client key for TLS (PEM-encoded)")
    
    46
    +              help="Private client key for TLS (PEM-encoded).")
    
    46 47
     @click.option('--client-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    47
    -              help="Public client certificate for TLS (PEM-encoded)")
    
    48
    +              help="Public client certificate for TLS (PEM-encoded).")
    
    48 49
     @click.option('--server-cert', type=click.Path(exists=True, dir_okay=False), default=None,
    
    49
    -              help="Public server certificate for TLS (PEM-encoded)")
    
    50
    +              help="Public server certificate for TLS (PEM-encoded).")
    
    50 51
     @click.option('--instance-name', type=click.STRING, default='main', show_default=True,
    
    51 52
                   help="Targeted farm instance name.")
    
    52 53
     @pass_context
    
    53
    -def cli(context, remote, instance_name, client_key, client_cert, server_cert):
    
    54
    -    url = urlparse(remote)
    
    54
    +def cli(context, remote, instance_name, auth_token, client_key, client_cert, server_cert):
    
    55
    +    """Entry point for the bgd-operation CLI command group."""
    
    56
    +    try:
    
    57
    +        context.channel, _ = setup_channel(remote, auth_token=auth_token,
    
    58
    +                                           client_key=client_key, client_cert=client_cert)
    
    55 59
     
    
    56
    -    context.remote = '{}:{}'.format(url.hostname, url.port or 50051)
    
    57
    -    context.instance_name = instance_name
    
    58
    -
    
    59
    -    if url.scheme == 'http':
    
    60
    -        context.channel = grpc.insecure_channel(context.remote)
    
    61
    -    else:
    
    62
    -        credentials = context.load_client_credentials(client_key, client_cert, server_cert)
    
    63
    -        if not credentials:
    
    64
    -            click.echo("ERROR: no TLS keys were specified and no defaults could be found.", err=True)
    
    65
    -            sys.exit(-1)
    
    60
    +    except InvalidArgumentError as e:
    
    61
    +        click.echo("Error: {}.".format(e), err=True)
    
    66 62
     
    
    67
    -        context.channel = grpc.secure_channel(context.remote, credentials)
    
    68
    -
    
    69
    -    click.echo("Starting for remote=[{}]".format(context.remote))
    
    63
    +    context.instance_name = instance_name
    
    70 64
     
    
    71 65
     
    
    72 66
     def _print_operation_status(operation, print_details=False):
    

  • buildgrid/client/authentication.py
    1
    +# Copyright (C) 2018 Bloomberg LP
    
    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
    +import base64
    
    17
    +from collections import namedtuple
    
    18
    +from urllib.parse import urlparse
    
    19
    +import os
    
    20
    +
    
    21
    +import grpc
    
    22
    +
    
    23
    +from buildgrid._exceptions import InvalidArgumentError
    
    24
    +from buildgrid.utils import read_file
    
    25
    +
    
    26
    +
    
    27
    +def load_tls_channel_credentials(client_key=None, client_cert=None, server_cert=None):
    
    28
    +    """Looks-up and loads TLS gRPC client channel credentials.
    
    29
    +
    
    30
    +    Args:
    
    31
    +        client_key(str, optional): Client certificate chain file path.
    
    32
    +        client_cert(str, optional): Client private key file path.
    
    33
    +        server_cert(str, optional): Serve root certificate file path.
    
    34
    +
    
    35
    +    Returns:
    
    36
    +        ChannelCredentials: Credentials to be used for a TLS-encrypted gRPC
    
    37
    +            client channel.
    
    38
    +    """
    
    39
    +    if server_cert and os.path.exists(server_cert):
    
    40
    +        server_cert_pem = read_file(server_cert)
    
    41
    +    else:
    
    42
    +        server_cert_pem = None
    
    43
    +        server_cert = None
    
    44
    +
    
    45
    +    if client_key and os.path.exists(client_key):
    
    46
    +        client_key_pem = read_file(client_key)
    
    47
    +    else:
    
    48
    +        client_key_pem = None
    
    49
    +        client_key = None
    
    50
    +
    
    51
    +    if client_key_pem and client_cert and os.path.exists(client_cert):
    
    52
    +        client_cert_pem = read_file(client_cert)
    
    53
    +    else:
    
    54
    +        client_cert_pem = None
    
    55
    +        client_cert = None
    
    56
    +
    
    57
    +    credentials = grpc.ssl_channel_credentials(root_certificates=server_cert_pem,
    
    58
    +                                               private_key=client_key_pem,
    
    59
    +                                               certificate_chain=client_cert_pem)
    
    60
    +
    
    61
    +    return credentials, (client_key, client_cert, server_cert,)
    
    62
    +
    
    63
    +
    
    64
    +def load_channel_authorization_token(auth_token=None):
    
    65
    +    """Looks-up and loads client authorization token.
    
    66
    +
    
    67
    +    Args:
    
    68
    +        auth_token (str, optional): Token file path.
    
    69
    +
    
    70
    +    Returns:
    
    71
    +        str: Encoded token string.
    
    72
    +    """
    
    73
    +    if auth_token and os.path.exists(auth_token):
    
    74
    +        return read_file(auth_token).decode()
    
    75
    +
    
    76
    +    # TODO: Try loading the token from a default location?
    
    77
    +
    
    78
    +    return None
    
    79
    +
    
    80
    +
    
    81
    +def setup_channel(remote_url, auth_token=None,
    
    82
    +                  client_key=None, client_cert=None, server_cert=None):
    
    83
    +    """Creates a new gRPC client communication chanel.
    
    84
    +
    
    85
    +    If `remote_url` does not specifies a port number, defaults 50051.
    
    86
    +
    
    87
    +    Args:
    
    88
    +        remote_url (str): URL for the remote, including port and protocol.
    
    89
    +        auth_token (str): Authorization token file path.
    
    90
    +        server_cert(str): TLS certificate chain file path.
    
    91
    +        client_key(str): TLS root certificate file path.
    
    92
    +        client_cert(str): TLS private key file path.
    
    93
    +
    
    94
    +    Returns:
    
    95
    +        Channel: Client Channel to be used in order to access the server
    
    96
    +            at `remote_url`.
    
    97
    +
    
    98
    +    Raises:
    
    99
    +        InvalidArgumentError: On any input parsing error.
    
    100
    +    """
    
    101
    +    url = urlparse(remote_url)
    
    102
    +    remote = '{}:{}'.format(url.hostname, url.port or 50051)
    
    103
    +    details = None, None, None
    
    104
    +
    
    105
    +    if url.scheme == 'http':
    
    106
    +        channel = grpc.insecure_channel(remote)
    
    107
    +
    
    108
    +    elif url.scheme == 'https':
    
    109
    +        credentials, details = load_tls_channel_credentials(client_key, client_cert, server_cert)
    
    110
    +        if not credentials:
    
    111
    +            raise InvalidArgumentError("Given TLS details (or defaults) could be loaded")
    
    112
    +
    
    113
    +        channel = grpc.secure_channel(remote, credentials)
    
    114
    +
    
    115
    +    else:
    
    116
    +        raise InvalidArgumentError("Given remote does not specify a protocol")
    
    117
    +
    
    118
    +    if auth_token is not None:
    
    119
    +        token = load_channel_authorization_token(auth_token)
    
    120
    +        if not token:
    
    121
    +            raise InvalidArgumentError("Given authorization token could be loaded")
    
    122
    +
    
    123
    +        interpector = AuthMetadataClientInterceptor(auth_token=token)
    
    124
    +        channel = grpc.intercept_channel(channel, interpector)
    
    125
    +
    
    126
    +    return channel, details
    
    127
    +
    
    128
    +
    
    129
    +class AuthMetadataClientInterceptor(
    
    130
    +        grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor,
    
    131
    +        grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor):
    
    132
    +
    
    133
    +    def __init__(self, auth_token=None, auth_secret=None):
    
    134
    +        """Initialises a new :class:`AuthMetadataClientInterceptor`.
    
    135
    +
    
    136
    +        Important:
    
    137
    +            One of `auth_token` or `auth_secret` must be provided.
    
    138
    +
    
    139
    +        Args:
    
    140
    +            auth_token (str, optional): Authorization token as a string.
    
    141
    +            auth_secret (str, optional): Authorization secret as a string.
    
    142
    +        """
    
    143
    +        if auth_token:
    
    144
    +            self.__secret = auth_token.strip()
    
    145
    +        else:
    
    146
    +            self.__secret = base64.b64encode(auth_secret)
    
    147
    +
    
    148
    +        self.__header_field_name = 'authorization'
    
    149
    +        self.__header_field_value = 'Bearer {}'.format(self.__secret)
    
    150
    +
    
    151
    +    # --- Public API ---
    
    152
    +
    
    153
    +    def intercept_unary_unary(self, continuation, client_call_details, request):
    
    154
    +        new_details = self._amend_call_details(client_call_details)
    
    155
    +
    
    156
    +        return continuation(new_details, request)
    
    157
    +
    
    158
    +    def intercept_unary_stream(self, continuation, client_call_details, request):
    
    159
    +        new_details = self._amend_call_details(client_call_details)
    
    160
    +
    
    161
    +        return continuation(new_details, request)
    
    162
    +
    
    163
    +    def intercept_stream_unary(self, continuation, client_call_details, request_iterator):
    
    164
    +        new_details = self._amend_call_details(client_call_details)
    
    165
    +
    
    166
    +        return continuation(new_details, request_iterator)
    
    167
    +
    
    168
    +    def intercept_stream_stream(self, continuation, client_call_details, request_iterator):
    
    169
    +        new_details = self._amend_call_details(client_call_details)
    
    170
    +
    
    171
    +        return continuation(new_details, request_iterator)
    
    172
    +
    
    173
    +    # --- Private API ---
    
    174
    +
    
    175
    +    def _amend_call_details(self, client_call_details):
    
    176
    +        """Appends an authorization field to given client call details."""
    
    177
    +        if client_call_details.metadata is not None:
    
    178
    +            new_metadata = list(client_call_details.metadata)
    
    179
    +        else:
    
    180
    +            new_metadata = []
    
    181
    +
    
    182
    +        new_metadata.append((self.__header_field_name, self.__header_field_value,))
    
    183
    +
    
    184
    +        class _ClientCallDetails(
    
    185
    +                namedtuple('_ClientCallDetails',
    
    186
    +                           ('method', 'timeout', 'credentials', 'metadata')),
    
    187
    +                grpc.ClientCallDetails):
    
    188
    +            pass
    
    189
    +
    
    190
    +        return _ClientCallDetails(client_call_details.method,
    
    191
    +                                  client_call_details.timeout,
    
    192
    +                                  client_call_details.credentials,
    
    193
    +                                  new_metadata)

  • buildgrid/server/_authentication.py
    1
    +# Copyright (C) 2018 Bloomberg LP
    
    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 datetime import datetime
    
    17
    +from enum import Enum
    
    18
    +import logging
    
    19
    +
    
    20
    +import grpc
    
    21
    +
    
    22
    +from buildgrid._exceptions import InvalidArgumentError
    
    23
    +
    
    24
    +
    
    25
    +try:
    
    26
    +    import jwt
    
    27
    +except ImportError:
    
    28
    +    HAVE_JWT = False
    
    29
    +else:
    
    30
    +    HAVE_JWT = True
    
    31
    +
    
    32
    +
    
    33
    +class AuthMetadataMethod(Enum):
    
    34
    +    # No authentication:
    
    35
    +    NONE = 'none'
    
    36
    +    # JWT based authentication:
    
    37
    +    JWT = 'JWT'
    
    38
    +
    
    39
    +
    
    40
    +class AuthMetadataAlgorithm(Enum):
    
    41
    +    # No encryption involved:
    
    42
    +    NONE = 'none'
    
    43
    +    # JWT related algorithms:
    
    44
    +    JWT_ES256 = 'ES256'  # ECDSA signature algorithm using SHA-256 hash algorithm
    
    45
    +    JWT_ES384 = 'ES384'  # ECDSA signature algorithm using SHA-384 hash algorithm
    
    46
    +    JWT_ES512 = 'ES512'  # ECDSA signature algorithm using SHA-512 hash algorithm
    
    47
    +    JWT_HS256 = 'HS256'  # HMAC using SHA-256 hash algorithm
    
    48
    +    JWT_HS384 = 'HS384'  # HMAC using SHA-384 hash algorithm
    
    49
    +    JWT_HS512 = 'HS512'  # HMAC using SHA-512 hash algorithm
    
    50
    +    JWT_PS256 = 'PS256'  # RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256
    
    51
    +    JWT_PS384 = 'PS384'  # RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384
    
    52
    +    JWT_PS512 = 'PS512'  # RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512
    
    53
    +    JWT_RS256 = 'RS256'  # RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash algorithm
    
    54
    +    JWT_RS384 = 'RS384'  # RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm
    
    55
    +    JWT_RS512 = 'RS512'  # RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm
    
    56
    +
    
    57
    +
    
    58
    +class _InvalidTokenError(Exception):
    
    59
    +    pass
    
    60
    +
    
    61
    +
    
    62
    +class _ExpiredTokenError(Exception):
    
    63
    +    pass
    
    64
    +
    
    65
    +
    
    66
    +class _UnboundedTokenError(Exception):
    
    67
    +    pass
    
    68
    +
    
    69
    +
    
    70
    +class AuthMetadataServerInterceptor(grpc.ServerInterceptor):
    
    71
    +
    
    72
    +    __auth_errors = {
    
    73
    +        'missing-bearer': 'Missing authentication header field',
    
    74
    +        'invalid-bearer': 'Invalid authentication header field',
    
    75
    +        'invalid-token': 'Invalid authentication token',
    
    76
    +        'expired-token': 'Expired authentication token',
    
    77
    +        'unbounded-token': 'Unbounded authentication token',
    
    78
    +    }
    
    79
    +
    
    80
    +    def __init__(self, method, secret=None, algorithm=AuthMetadataAlgorithm.NONE):
    
    81
    +        """Initialises a new :class:`AuthMetadataServerInterceptor`.
    
    82
    +
    
    83
    +        Args:
    
    84
    +            method (AuthMetadataMethod): Type of authorization method.
    
    85
    +            secret (str): The secret or key to be used for validating request,
    
    86
    +                depending on `method`. Defaults to ``None``.
    
    87
    +            algorithm (AuthMetadataAlgorithm): The crytographic algorithm used
    
    88
    +                to encode `secret`. Defaults to ``AuthMetadataAlgorithm.NONE``.
    
    89
    +
    
    90
    +        Raises:
    
    91
    +            InvalidArgumentError: If `method` is not supported or if `algorithm`
    
    92
    +                is not supported for the given `method`.
    
    93
    +        """
    
    94
    +        self.__logger = logging.getLogger(__name__)
    
    95
    +
    
    96
    +        self.__bearer_cache = {}
    
    97
    +        self.__terminators = {}
    
    98
    +        self.__validator = None
    
    99
    +        self.__secret = secret
    
    100
    +
    
    101
    +        self._method = method
    
    102
    +        self._algorithm = algorithm
    
    103
    +
    
    104
    +        if self._method == AuthMetadataMethod.JWT:
    
    105
    +            if not HAVE_JWT:
    
    106
    +                raise InvalidArgumentError("JWT authorization method requires PyJWT")
    
    107
    +
    
    108
    +            try:
    
    109
    +                jwt.register_algorithm(self._algorithm.value, None)
    
    110
    +            except TypeError:
    
    111
    +                raise InvalidArgumentError("Algorithm not supported for JWT decoding: [{}]"
    
    112
    +                                           .format(self._algorithm))
    
    113
    +            except ValueError:
    
    114
    +                pass
    
    115
    +
    
    116
    +            self.__validator = self._validate_jwt_token
    
    117
    +
    
    118
    +        for code, message in self.__auth_errors.items():
    
    119
    +            self.__terminators[code] = _unary_unary_rpc_terminator(message)
    
    120
    +
    
    121
    +    # --- Public API ---
    
    122
    +
    
    123
    +    @property
    
    124
    +    def method(self):
    
    125
    +        return self._method
    
    126
    +
    
    127
    +    @property
    
    128
    +    def algorithm(self):
    
    129
    +        return self._algorithm
    
    130
    +
    
    131
    +    def intercept_service(self, continuation, handler_call_details):
    
    132
    +        try:
    
    133
    +            # Reject requests not carrying a token:
    
    134
    +            bearer = dict(handler_call_details.invocation_metadata)['authorization']
    
    135
    +
    
    136
    +        except KeyError:
    
    137
    +            self.__logger.error("Rejecting '{}' request: {}"
    
    138
    +                                .format(handler_call_details.method.split('/')[-1],
    
    139
    +                                        self.__auth_errors['missing-bearer']))
    
    140
    +            return self.__terminators['missing-bearer']
    
    141
    +
    
    142
    +        # Reject requests with malformated bearer:
    
    143
    +        if not bearer.startswith('Bearer '):
    
    144
    +            self.__logger.error("Rejecting '{}' request: {}"
    
    145
    +                                .format(handler_call_details.method.split('/')[-1],
    
    146
    +                                        self.__auth_errors['invalid-bearer']))
    
    147
    +            return self.__terminators['invalid-bearer']
    
    148
    +
    
    149
    +        try:
    
    150
    +            # Hit the cache for already validated token:
    
    151
    +            expiration_time = self.__bearer_cache[bearer]
    
    152
    +
    
    153
    +            # Accept request if cached token hasn't expired yet:
    
    154
    +            if expiration_time < datetime.utcnow():
    
    155
    +                return continuation(handler_call_details)  # Accepted
    
    156
    +
    
    157
    +        except KeyError:
    
    158
    +            pass
    
    159
    +
    
    160
    +        assert self.__validator is not None
    
    161
    +
    
    162
    +        try:
    
    163
    +            # Decode and validate the new token:
    
    164
    +            expiration_time = self.__validator(bearer[7:])
    
    165
    +
    
    166
    +        except _InvalidTokenError as e:
    
    167
    +            self.__logger.error("Rejecting '{}' request: {}; {}"
    
    168
    +                                .format(handler_call_details.method.split('/')[-1],
    
    169
    +                                        self.__auth_errors['invalid-token'], str(e)))
    
    170
    +            return self.__terminators['invalid-token']
    
    171
    +
    
    172
    +        except _ExpiredTokenError as e:
    
    173
    +            self.__logger.error("Rejecting '{}' request: {}; {}"
    
    174
    +                                .format(handler_call_details.method.split('/')[-1],
    
    175
    +                                        self.__auth_errors['expired-token'], str(e)))
    
    176
    +            return self.__terminators['expired-token']
    
    177
    +
    
    178
    +        except _UnboundedTokenError as e:
    
    179
    +            self.__logger.error("Rejecting '{}' request: {}; {}"
    
    180
    +                                .format(handler_call_details.method.split('/')[-1],
    
    181
    +                                        self.__auth_errors['unbounded-token'], str(e)))
    
    182
    +            return self.__terminators['unbounded-token']
    
    183
    +
    
    184
    +        # Cache the validated token and store expiration time:
    
    185
    +        self.__bearer_cache[bearer] = expiration_time
    
    186
    +
    
    187
    +        return continuation(handler_call_details)  # Accepted
    
    188
    +
    
    189
    +    # --- Private API ---
    
    190
    +
    
    191
    +    def _validate_jwt_token(self, token):
    
    192
    +        """Validates a JWT token and returns its expiry date."""
    
    193
    +        try:
    
    194
    +            payload = jwt.decode(
    
    195
    +                token, self.__secret, algorithms=[self._algorithm.value])
    
    196
    +
    
    197
    +        except jwt.exceptions.ExpiredSignatureError as e:
    
    198
    +            raise _ExpiredTokenError(e)
    
    199
    +
    
    200
    +        except jwt.exceptions.InvalidTokenError as e:
    
    201
    +            raise _InvalidTokenError(e)
    
    202
    +
    
    203
    +        if 'exp' not in payload or not isinstance(payload['exp'], int):
    
    204
    +            raise _UnboundedTokenError("Missing 'exp' in payload")
    
    205
    +
    
    206
    +        return datetime.fromtimestamp(payload['exp'])
    
    207
    +
    
    208
    +
    
    209
    +def _unary_unary_rpc_terminator(details):
    
    210
    +
    
    211
    +    def terminate(ignored_request, context):
    
    212
    +        context.abort(grpc.StatusCode.UNAUTHENTICATED, details)
    
    213
    +
    
    214
    +    return grpc.unary_unary_rpc_method_handler(terminate)

  • buildgrid/server/instance.py
    ... ... @@ -29,6 +29,7 @@ import janus
    29 29
     from buildgrid._enums import BotStatus, LogRecordLevel, MetricRecordDomain, MetricRecordType
    
    30 30
     from buildgrid._protos.buildgrid.v2 import monitoring_pb2
    
    31 31
     from buildgrid.server.actioncache.service import ActionCacheService
    
    32
    +from buildgrid.server._authentication import AuthMetadataMethod, AuthMetadataAlgorithm, AuthMetadataServerInterceptor
    
    32 33
     from buildgrid.server.bots.service import BotsService
    
    33 34
     from buildgrid.server.capabilities.instance import CapabilitiesInstance
    
    34 35
     from buildgrid.server.capabilities.service import CapabilitiesService
    
    ... ... @@ -47,11 +48,21 @@ class BuildGridServer:
    47 48
         requisite services.
    
    48 49
         """
    
    49 50
     
    
    50
    -    def __init__(self, max_workers=None, monitor=False):
    
    51
    +    def __init__(self, max_workers=None, monitor=False, auth_method=AuthMetadataMethod.NONE,
    
    52
    +                 auth_secret=None, auth_algorithm=AuthMetadataAlgorithm.NONE):
    
    51 53
             """Initializes a new :class:`BuildGridServer` instance.
    
    52 54
     
    
    53 55
             Args:
    
    54 56
                 max_workers (int, optional): A pool of max worker threads.
    
    57
    +            monitor (bool, optional): Whether or not to globally activate server
    
    58
    +                monitoring. Defaults to ``False``.
    
    59
    +            auth_method (AuthMetadataMethod, optional): Authentication method to
    
    60
    +                be used for request authorization. Defaults to ``NONE``.
    
    61
    +            auth_secret (str, optional): The secret or key to be used for
    
    62
    +                authorizing request using `auth_method`. Defaults to ``None``.
    
    63
    +            auth_algorithm (AuthMetadataAlgorithm, optional): The crytographic
    
    64
    +                algorithm to be uses in combination with `auth_secret` for
    
    65
    +                authorizing request using `auth_method`. Defaults to ``NONE``.
    
    55 66
             """
    
    56 67
             self.__logger = logging.getLogger(__name__)
    
    57 68
     
    
    ... ... @@ -59,8 +70,17 @@ class BuildGridServer:
    59 70
                 # Use max_workers default from Python 3.5+
    
    60 71
                 max_workers = (os.cpu_count() or 1) * 5
    
    61 72
     
    
    73
    +        self.__grpc_auth_interceptor = None
    
    74
    +        if auth_method != AuthMetadataMethod.NONE:
    
    75
    +            self.__grpc_auth_interceptor = AuthMetadataServerInterceptor(
    
    76
    +                method=auth_method, secret=auth_secret, algorithm=auth_algorithm)
    
    62 77
             self.__grpc_executor = futures.ThreadPoolExecutor(max_workers)
    
    63
    -        self.__grpc_server = grpc.server(self.__grpc_executor)
    
    78
    +
    
    79
    +        if self.__grpc_auth_interceptor is not None:
    
    80
    +            self.__grpc_server = grpc.server(
    
    81
    +                self.__grpc_executor, interceptors=(self.__grpc_auth_interceptor,))
    
    82
    +        else:
    
    83
    +            self.__grpc_server = grpc.server(self.__grpc_executor)
    
    64 84
     
    
    65 85
             self.__main_loop = asyncio.get_event_loop()
    
    66 86
     
    

  • docs/source/installation.rst
    ... ... @@ -59,20 +59,20 @@ have to adjust your ``PATH``, in ``~/.bashrc``, with:
    59 59
     
    
    60 60
     .. note::
    
    61 61
     
    
    62
    -   The ``setup.py`` script defines two extra targets, ``docs`` and ``tests``,
    
    63
    -   that declare required dependency for, respectively, generating documentation
    
    64
    -   and running unit-tests. They can be use as helpers for setting up a
    
    65
    -   development environment. To use them simply run:
    
    62
    +   The ``setup.py`` script defines three extra targets, ``auth``, ``docs`` and
    
    63
    +   ``tests``. They declare required dependency for, respectively, authentication
    
    64
    +   and authorization management, generating documentation and running
    
    65
    +   unit-tests. They can be use as helpers for setting up a development
    
    66
    +   environment. To use them simply run:
    
    66 67
     
    
    67 68
        .. code-block:: sh
    
    68 69
     
    
    69
    -      pip3 install --user --editable ".[docs,tests]"
    
    70
    -
    
    70
    +      pip3 install --user --editable ".[auth,docs,tests]"
    
    71 71
     
    
    72 72
     
    
    73 73
     .. install-docker:
    
    74 74
     
    
    75
    -Installation through docker
    
    75
    +Installation through Docker
    
    76 76
     ---------------------------
    
    77 77
     
    
    78 78
     How to build a Docker image that runs BuildGrid.
    

  • setup.py
    ... ... @@ -85,6 +85,11 @@ def get_cmdclass():
    85 85
         }
    
    86 86
         return cmdclass
    
    87 87
     
    
    88
    +auth_require = [
    
    89
    +    'cryptography >= 1.8.0',  # Required by pyjwt for RSA
    
    90
    +    'pyjwt >= 1.5.0',
    
    91
    +]
    
    92
    +
    
    88 93
     tests_require = [
    
    89 94
         'coverage >= 4.5.0',
    
    90 95
         'moto < 1.3.7',
    
    ... ... @@ -130,6 +135,7 @@ setup(
    130 135
         setup_requires=['pytest-runner'],
    
    131 136
         tests_require=tests_require,
    
    132 137
         extras_require={
    
    138
    +        'auth': auth_require,
    
    133 139
             'docs': docs_require,
    
    134 140
             'tests': tests_require,
    
    135 141
         },
    

  • tests/auth/__init__.py

  • tests/auth/data/jwt-hs256-conflicting.secret
    1
    +not-your-256-bit-secret
    \ No newline at end of file

  • tests/auth/data/jwt-hs256-expired.token
    1
    +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJCdWlsZEdyaWQgVGVzdCIsImlhdCI6MTM1NTMxNDMzMiwiZXhwIjoxMzU1MzE0MzMyfQ.J-YxkvVbeZFwZ2_mK1d91Wb5vm481LkuehfkUoNpLb8
    \ No newline at end of file

  • tests/auth/data/jwt-hs256-matching.secret
    1
    +your-256-bit-secret

  • tests/auth/data/jwt-hs256-unbounded.token
    1
    +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJCdWlsZEdyaWQgVGVzdCIsImlhdCI6MTM1NTMxNDMzMn0.tEBYb8GM_4lmJCxrh6iMhdoxupgGTCaJR3atLxodZV8
    \ No newline at end of file

  • tests/auth/data/jwt-hs256-valid.token
    1
    +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJCdWlsZEdyaWQgVGVzdCIsImlhdCI6MTM1NTMxNDMzMiwiZXhwIjoyMzAxOTk5MTMyfQ.U__BJgksk65S0YuDZqU85tRWF3F03ITF9HXW1cd_Ci8
    \ No newline at end of file

  • tests/auth/data/jwt-rs256-conflicting.pub.key
    1
    +-----BEGIN PUBLIC KEY-----
    
    2
    +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNGWI+5SgYIiGcS5UNQzZOs5As
    
    3
    +QJRsKR7bBiMjJWnqrNExn0pYrecQsBy6zgwmN6MqEPFV5A1GOEeP3FAqlwD5y+rL
    
    4
    +iO2tqq0naiFiJb27qsHzgjakw7pVqgJuGuLVIWWE1RlDnhfN+auNGWyl2YjF6N2+
    
    5
    +Lsl0bBX8Q8zaOxbU3wIDAQAB
    
    6
    +-----END PUBLIC KEY-----

  • tests/auth/data/jwt-rs256-expired.token
    1
    +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJCdWlsZEdyaWQgVGVzdCIsImlhdCI6MTM1NTMxNDMzMiwiZXhwIjoxMzU1MzE0MzMyfQ.L9Wg6YDEV57_a_j_J2s6ug5eEGS80SvlwztdVFzU1ajqlfYmbF5oYq6IEMaiZq95aKkJ71xnwpGfpcuufH-xONiNoZhTg7r-lb99yvPZ8VEHwOIyt1EUEziffim9XRiuwM570fg7_HUC-ZhNJG536k1IM-6rPRnv-1Tu-MxLgvQ
    \ No newline at end of file

  • tests/auth/data/jwt-rs256-matching.priv.key
    1
    +-----BEGIN RSA PRIVATE KEY-----
    
    2
    +MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw
    
    3
    +33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW
    
    4
    ++jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
    
    5
    +AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS
    
    6
    +3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp
    
    7
    +uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE
    
    8
    +2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0
    
    9
    +GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K
    
    10
    +Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY
    
    11
    +6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5
    
    12
    +fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523
    
    13
    +Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP
    
    14
    +FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==
    
    15
    +-----END RSA PRIVATE KEY-----
    \ No newline at end of file

  • tests/auth/data/jwt-rs256-matching.pub.key
    1
    +-----BEGIN PUBLIC KEY-----
    
    2
    +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd
    
    3
    +UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs
    
    4
    +HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D
    
    5
    +o2kQ+X5xK9cipRgEKwIDAQAB
    
    6
    +-----END PUBLIC KEY-----

  • tests/auth/data/jwt-rs256-unbounded.token
    1
    +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJCdWlsZEdyaWQgVGVzdCIsImlhdCI6MTM1NTMxNDMzMn0.1_yoF5Vg1fXs2SmWhYm7LbLMGbZVgBjxZkzRlw87blSG6lRgEi_R-WXKcS4n_pGynkcahJ_AqLseyHduXZveI1nVQFATXVNQQPcvmkM6pYSHPm155iqZFdYAWWVKB9ND1F3oDrXLBzqF2a4HtLNXYTu5gDStdUPkrH_FiatX79g
    \ No newline at end of file

  • tests/auth/data/jwt-rs256-valid.token
    1
    +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJCdWlsZEdyaWQgVGVzdCIsImlhdCI6MTM1NTMxNDMzMiwiZXhwIjoyMzAxOTk5MTMyfQ.RCbQNqPaF0mfFsUGwdb47Ga3DITAL7OjYlcWkJ2xWL61Fo9zURx_mSIVDYTgEY3nFW1cmf2r6Y2Z0rEUOY-qBl8B9Ww9jijGz7LMR4w_j8f967MjTxkyOCWZSUWazObg5jxNjLtfxPD28UbbrS_2R8BLgMQFf3ymSDXTX5lAdBY
    \ No newline at end of file

  • tests/auth/test_client.py
    1
    +# Copyright (C) 2018 Bloomberg LP
    
    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
    +# pylint: disable=redefined-outer-name
    
    16
    +
    
    17
    +
    
    18
    +import os
    
    19
    +
    
    20
    +import grpc
    
    21
    +import pytest
    
    22
    +
    
    23
    +from buildgrid.client.authentication import setup_channel
    
    24
    +from buildgrid._protos.google.bytestream import bytestream_pb2, bytestream_pb2_grpc
    
    25
    +from buildgrid.server._authentication import AuthMetadataMethod, AuthMetadataAlgorithm
    
    26
    +
    
    27
    +from ..utils.dummy import serve_dummy
    
    28
    +from ..utils.utils import run_in_subprocess
    
    29
    +
    
    30
    +
    
    31
    +try:
    
    32
    +    import jwt  # pylint: disable=unused-import
    
    33
    +except ImportError:
    
    34
    +    HAVE_JWT = False
    
    35
    +else:
    
    36
    +    HAVE_JWT = True
    
    37
    +
    
    38
    +
    
    39
    +DATA_DIR = os.path.join(
    
    40
    +    os.path.dirname(os.path.realpath(__file__)), 'data')
    
    41
    +
    
    42
    +METHOD = AuthMetadataMethod.JWT
    
    43
    +TOKEN = os.path.join(DATA_DIR, 'jwt-hs256-valid.token')
    
    44
    +SECRET = 'your-256-bit-secret'
    
    45
    +ALGORITHM = AuthMetadataAlgorithm.JWT_HS256
    
    46
    +
    
    47
    +
    
    48
    +@pytest.mark.skipif(not HAVE_JWT, reason="No pyjwt")
    
    49
    +def test_channel_token_authorization():
    
    50
    +    # Actual test function, to be run in a subprocess:
    
    51
    +    def __test_channel_token_authorization(queue, remote, token):
    
    52
    +        channel, _ = setup_channel(remote, auth_token=token)
    
    53
    +        stub = bytestream_pb2_grpc.ByteStreamStub(channel)
    
    54
    +
    
    55
    +        request = bytestream_pb2.QueryWriteStatusRequest()
    
    56
    +        status_code = grpc.StatusCode.OK
    
    57
    +
    
    58
    +        try:
    
    59
    +            next(stub.Read(request))
    
    60
    +
    
    61
    +        except grpc.RpcError as e:
    
    62
    +            status_code = e.code()
    
    63
    +
    
    64
    +        queue.put(status_code)
    
    65
    +
    
    66
    +    with serve_dummy(auth_method=METHOD, auth_secret=SECRET,
    
    67
    +                     auth_algorithm=ALGORITHM) as server:
    
    68
    +        status = run_in_subprocess(__test_channel_token_authorization,
    
    69
    +                                   server.remote, TOKEN)
    
    70
    +
    
    71
    +        assert status != grpc.StatusCode.UNAUTHENTICATED

  • tests/auth/test_interceptor.py
    1
    +# Copyright (C) 2018 Bloomberg LP
    
    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
    +# pylint: disable=redefined-outer-name
    
    16
    +
    
    17
    +
    
    18
    +from collections import namedtuple
    
    19
    +from unittest import mock
    
    20
    +import os
    
    21
    +
    
    22
    +import grpc
    
    23
    +from grpc._server import _Context
    
    24
    +import pytest
    
    25
    +
    
    26
    +from buildgrid.server._authentication import AuthMetadataMethod, AuthMetadataAlgorithm
    
    27
    +from buildgrid.server._authentication import AuthMetadataServerInterceptor
    
    28
    +
    
    29
    +from ..utils.utils import read_file
    
    30
    +
    
    31
    +
    
    32
    +try:
    
    33
    +    import jwt  # pylint: disable=unused-import
    
    34
    +except ImportError:
    
    35
    +    HAVE_JWT = False
    
    36
    +else:
    
    37
    +    HAVE_JWT = True
    
    38
    +
    
    39
    +
    
    40
    +DATA_DIR = os.path.join(
    
    41
    +    os.path.dirname(os.path.realpath(__file__)), 'data')
    
    42
    +
    
    43
    +TOKENS = [None, 'not-a-token']
    
    44
    +SECRETS = [None, None]
    
    45
    +ALGORITHMS = [AuthMetadataAlgorithm.NONE, AuthMetadataAlgorithm.NONE]
    
    46
    +VALIDITIES = [False, False]
    
    47
    +# Generic test data: token, secret, algorithm, validity:
    
    48
    +DATA = zip(TOKENS, SECRETS, ALGORITHMS, VALIDITIES)
    
    49
    +
    
    50
    +JWT_TOKENS = [
    
    51
    +    'jwt-hs256-expired.token',
    
    52
    +    'jwt-hs256-unbounded.token',
    
    53
    +    'jwt-hs256-valid.token',
    
    54
    +    'jwt-hs256-valid.token',
    
    55
    +
    
    56
    +    'jwt-rs256-expired.token',
    
    57
    +    'jwt-rs256-unbounded.token',
    
    58
    +    'jwt-rs256-valid.token',
    
    59
    +    'jwt-rs256-valid.token']
    
    60
    +JWT_SECRETS = [
    
    61
    +    'jwt-hs256-matching.secret',
    
    62
    +    'jwt-hs256-matching.secret',
    
    63
    +    'jwt-hs256-matching.secret',
    
    64
    +    'jwt-hs256-conflicting.secret',
    
    65
    +
    
    66
    +    'jwt-rs256-matching.pub.key',
    
    67
    +    'jwt-rs256-matching.pub.key',
    
    68
    +    'jwt-rs256-matching.pub.key',
    
    69
    +    'jwt-rs256-conflicting.pub.key']
    
    70
    +JWT_ALGORITHMS = [
    
    71
    +    AuthMetadataAlgorithm.JWT_HS256,
    
    72
    +    AuthMetadataAlgorithm.JWT_HS256,
    
    73
    +    AuthMetadataAlgorithm.JWT_HS256,
    
    74
    +    AuthMetadataAlgorithm.JWT_HS256,
    
    75
    +
    
    76
    +    AuthMetadataAlgorithm.JWT_RS256,
    
    77
    +    AuthMetadataAlgorithm.JWT_RS256,
    
    78
    +    AuthMetadataAlgorithm.JWT_RS256,
    
    79
    +    AuthMetadataAlgorithm.JWT_RS256]
    
    80
    +JWT_VALIDITIES = [
    
    81
    +    False, False, True, False,
    
    82
    +    False, False, True, False]
    
    83
    +# JWT test data: token, secret, algorithm, validity:
    
    84
    +JWT_DATA = zip(JWT_TOKENS, JWT_SECRETS, JWT_ALGORITHMS, JWT_VALIDITIES)
    
    85
    +
    
    86
    +
    
    87
    +_MockHandlerCallDetails = namedtuple(
    
    88
    +    '_MockHandlerCallDetails', ('method', 'invocation_metadata',))
    
    89
    +_MockMetadatum = namedtuple(
    
    90
    +    '_MockMetadatum', ('key', 'value',))
    
    91
    +
    
    92
    +
    
    93
    +def _mock_call_details(token, method='TestMethod'):
    
    94
    +    invocation_metadata = [
    
    95
    +        _MockMetadatum(
    
    96
    +            key='user-agent',
    
    97
    +            value='grpc-c/6.0.0 (manylinux; chttp2; gao)')]
    
    98
    +
    
    99
    +    if token and token.count('.') == 2:
    
    100
    +        invocation_metadata.append(_MockMetadatum(
    
    101
    +            key='authorization', value='Bearer {}'.format(token)))
    
    102
    +
    
    103
    +    elif token:
    
    104
    +        invocation_metadata.append(_MockMetadatum(
    
    105
    +            key='authorization', value=token))
    
    106
    +
    
    107
    +    return _MockHandlerCallDetails(
    
    108
    +        method=method, invocation_metadata=invocation_metadata)
    
    109
    +
    
    110
    +
    
    111
    +def _unary_unary_rpc_terminator(details):
    
    112
    +
    
    113
    +    def terminate(ignored_request, context):
    
    114
    +        context.set_code(grpc.StatusCode.OK)
    
    115
    +
    
    116
    +    return grpc.unary_unary_rpc_method_handler(terminate)
    
    117
    +
    
    118
    +
    
    119
    +@pytest.mark.parametrize('token,secret,algorithm,validity', DATA)
    
    120
    +def test_authorization(token, secret, algorithm, validity):
    
    121
    +    interceptor = AuthMetadataServerInterceptor(
    
    122
    +        method=AuthMetadataMethod.NONE, secret=secret, algorithm=algorithm)
    
    123
    +
    
    124
    +    call_details = _mock_call_details(token)
    
    125
    +    context = mock.create_autospec(_Context, spec_set=True)
    
    126
    +
    
    127
    +    try:
    
    128
    +        handler = interceptor.intercept_service(None, call_details)
    
    129
    +
    
    130
    +    except AssertionError:
    
    131
    +        context.set_code(grpc.StatusCode.OK)
    
    132
    +
    
    133
    +    else:
    
    134
    +        handler.unary_unary(None, context)
    
    135
    +
    
    136
    +    if validity:
    
    137
    +        context.set_code.assert_called_once_with(grpc.StatusCode.OK)
    
    138
    +        context.abort.assert_not_called()
    
    139
    +
    
    140
    +    else:
    
    141
    +        context.abort.assert_called_once_with(grpc.StatusCode.UNAUTHENTICATED, mock.ANY)
    
    142
    +        context.set_code.assert_not_called()
    
    143
    +
    
    144
    +
    
    145
    +@pytest.mark.skipif(not HAVE_JWT, reason="No pyjwt")
    
    146
    +@pytest.mark.parametrize('token,secret,algorithm,validity', JWT_DATA)
    
    147
    +def test_jwt_authorization(token, secret, algorithm, validity):
    
    148
    +    token = read_file(os.path.join(DATA_DIR, token), text_mode=True).strip()
    
    149
    +    secret = read_file(os.path.join(DATA_DIR, secret), text_mode=True).strip()
    
    150
    +
    
    151
    +    interceptor = AuthMetadataServerInterceptor(
    
    152
    +        method=AuthMetadataMethod.JWT, secret=secret, algorithm=algorithm)
    
    153
    +
    
    154
    +    continuator = _unary_unary_rpc_terminator
    
    155
    +    call_details = _mock_call_details(token)
    
    156
    +    context = mock.create_autospec(_Context, spec_set=True)
    
    157
    +
    
    158
    +    handler = interceptor.intercept_service(continuator, call_details)
    
    159
    +    handler.unary_unary(None, context)
    
    160
    +
    
    161
    +    if validity:
    
    162
    +        context.set_code.assert_called_once_with(grpc.StatusCode.OK)
    
    163
    +        context.abort.assert_not_called()
    
    164
    +
    
    165
    +    else:
    
    166
    +        context.abort.assert_called_once_with(grpc.StatusCode.UNAUTHENTICATED, mock.ANY)
    
    167
    +        context.set_code.assert_not_called()

  • tests/utils/dummy.py
    1
    +# Copyright (C) 2018 Bloomberg LP
    
    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 concurrent import futures
    
    17
    +from contextlib import contextmanager
    
    18
    +import multiprocessing
    
    19
    +import os
    
    20
    +import signal
    
    21
    +
    
    22
    +import grpc
    
    23
    +import pytest_cov
    
    24
    +
    
    25
    +from buildgrid.server._authentication import AuthMetadataMethod, AuthMetadataAlgorithm
    
    26
    +from buildgrid.server._authentication import AuthMetadataServerInterceptor
    
    27
    +
    
    28
    +
    
    29
    +@contextmanager
    
    30
    +def serve_dummy(auth_method=AuthMetadataMethod.NONE,
    
    31
    +                auth_secret=None, auth_algorithm=AuthMetadataAlgorithm.NONE):
    
    32
    +    server = Server(
    
    33
    +        auth_method=auth_method, auth_secret=auth_secret, auth_algorithm=auth_algorithm)
    
    34
    +    try:
    
    35
    +        yield server
    
    36
    +    finally:
    
    37
    +        server.quit()
    
    38
    +
    
    39
    +
    
    40
    +class Server:
    
    41
    +
    
    42
    +    def __init__(self, auth_method=AuthMetadataMethod.NONE,
    
    43
    +                 auth_secret=None, auth_algorithm=AuthMetadataAlgorithm.NONE):
    
    44
    +
    
    45
    +        self.__queue = multiprocessing.Queue()
    
    46
    +        self.__auth_interceptor = None
    
    47
    +        if auth_method != AuthMetadataMethod.NONE:
    
    48
    +            self.__auth_interceptor = AuthMetadataServerInterceptor(
    
    49
    +                method=auth_method, secret=auth_secret, algorithm=auth_algorithm)
    
    50
    +        self.__process = multiprocessing.Process(
    
    51
    +            target=Server.serve, args=(self.__queue, self.__auth_interceptor))
    
    52
    +        self.__process.start()
    
    53
    +
    
    54
    +        self.port = self.__queue.get()
    
    55
    +        self.remote = 'http://localhost:{}'.format(self.port)
    
    56
    +
    
    57
    +    @classmethod
    
    58
    +    def serve(cls, queue, auth_interceptor):
    
    59
    +        pytest_cov.embed.cleanup_on_sigterm()
    
    60
    +
    
    61
    +        # Use max_workers default from Python 3.5+
    
    62
    +        max_workers = (os.cpu_count() or 1) * 5
    
    63
    +        executor = futures.ThreadPoolExecutor(max_workers)
    
    64
    +        if auth_interceptor is not None:
    
    65
    +            server = grpc.server(executor, interceptors=(auth_interceptor,))
    
    66
    +        else:
    
    67
    +            server = grpc.server(executor)
    
    68
    +        port = server.add_insecure_port('localhost:0')
    
    69
    +
    
    70
    +        queue.put(port)
    
    71
    +
    
    72
    +        server.start()
    
    73
    +
    
    74
    +        signal.pause()
    
    75
    +
    
    76
    +    def quit(self):
    
    77
    +        if self.__process:
    
    78
    +            self.__process.terminate()
    
    79
    +            self.__process.join()

  • tests/utils/utils.py
    ... ... @@ -34,6 +34,11 @@ def kill_process_tree(pid):
    34 34
         kill_proc(proc)
    
    35 35
     
    
    36 36
     
    
    37
    +def read_file(file_path, text_mode=False):
    
    38
    +    with open(file_path, 'r' if text_mode else 'r') as in_file:
    
    39
    +        return in_file.read()
    
    40
    +
    
    41
    +
    
    37 42
     def run_in_subprocess(function, *arguments, timeout=1):
    
    38 43
         queue = multiprocessing.Queue()
    
    39 44
         # Use subprocess to avoid creation of gRPC threads in main process
    



  • [Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]