diff --git a/deployments/aws/Makefile b/deployments/aws/Makefile new file mode 100644 index 00000000..bc4fb310 --- /dev/null +++ b/deployments/aws/Makefile @@ -0,0 +1,2 @@ +THIS_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +SCRIPT := $(THIS_DIR)/deployment.sh diff --git a/deployments/aws/bin/veraison b/deployments/aws/bin/veraison new file mode 100755 index 00000000..6712ecc6 --- /dev/null +++ b/deployments/aws/bin/veraison @@ -0,0 +1,2016 @@ +#!/usr/bin/env python +# pyright: reportOptionalMemberAccess=false +# pyright: reportOptionalSubscript=false +import argparse +import asyncio +import getpass +import inspect +import json +import logging +import os +import pprint +import random +import re +import shutil +import socket +import stat +import string +import sys +import time +from asyncio.subprocess import Process, PIPE +from copy import copy +from datetime import datetime, timedelta, timezone +from urllib.parse import urlparse + +import boto3 +import fabric +import psycopg2 +import requests +import xdg.BaseDirectory +import yaml +from botocore.exceptions import ClientError +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509.oid import NameOID +from envsubst import envsubst +from sqlitedict import SqliteDict + + +COLOR_DARK_GREY = '\x1b[38;5;245m' +COLOR_MEDIUM_GREY = '\x1b[38;5;250m' +COLOR_GREY = '\x1b[38;20m' +COLOR_GREEN = '\x1b[38;5;2m' +COLOR_YELLOW = '\x1b[33;20m' +COLOR_RED = '\x1b[31;20m' +COLOR_BOLD_RED = '\x1b[31;1m' +COLOR_RESET = '\x1b[0m' +']]]]]]]]' # "close" the brackets above to fix nvim's auto-indent + + +class Aws: + + @property + def ec2(self): + if self._ec2 is None: + self._ec2 = self.session.client('ec2') + return self._ec2 + + @property + def cf(self): + if self._cf is None: + self._cf = self.session.client('cloudformation') + return self._cf + + @property + def rds(self): + if self._rds is None: + self._rds = self.session.client('rds') + return self._rds + + def __init__(self, **kwargs): + self.session = boto3.Session(**kwargs) + self._ec2 = None + self._cf = None + self._rds = None + + def close(self): + if self._ec2 is not None: + self._ec2.close() + self._ec2 = None + if self._cf is not None: + self._cf.close() + self._cf = None + if self._rds is not None: + self._rds.close() + self._rds = None + + def __del__(self): + self.close() + + +def randomword(n=32, use_punctuation=False): + alphabet = string.ascii_letters + string.digits + if use_punctuation: + alphabet += string.punctuation + return ''.join(random.choice(alphabet) for _ in range(n)) + + +def get_public_ip_address(): + resp = requests.get('http://ifconfig.me') + if resp.status_code != 200: + raise RuntimeError( + f'could not access http://ifconfig.me: {resp.reason} ({resp.status_code})') + return resp.text + + +def authorize_ports_for_address(aws, group_id, addr, ports, tag, deployment_tags): + permissions = [] + for port in ports: + permissions.append({ + 'FromPort': port, + 'ToPort': port, + 'IpProtocol': 'tcp', + 'IpRanges': [{'CidrIp': f'{addr}/32'}], + }) + + tags = copy(deployment_tags) + tags.append({'Key': 'dynamic-address', 'Value': tag}) + + aws.ec2.authorize_security_group_ingress( + GroupId=group_id, + IpPermissions=permissions, + TagSpecifications=[ + { + 'ResourceType': 'security-group-rule', + 'Tags': tags, + } + ], + ) + + +def revoke_security_group_rules_by_tag(aws, group_id, tag): + resp = aws.ec2.describe_security_group_rules( + Filters=[ + { + 'Name': 'group-id', + 'Values': [group_id], + }, + { + 'Name': 'tag:dynamic-address', + 'Values': [tag], + } + ], + ) + + rule_ids = [sgr['SecurityGroupRuleId'] for sgr in resp['SecurityGroupRules']] + if not rule_ids: + return + + aws.ec2.revoke_security_group_ingress( + GroupId=group_id, + SecurityGroupRuleIds=rule_ids, + ) + + +def command_create_packer_security_group(cmd, deployment_name): + vpc_id = cmd.cache.get('vpc-id') + if not vpc_id: + cmd.fail('vpc-id has not been configured') + + cmd.logger.debug('creating Packer security group...') + resp = cmd.aws.ec2.create_security_group( + GroupName='veraison-packer', + Description='Temporary security group for creating Veraison packer imeages', + VpcId=vpc_id, + TagSpecifications=[ + { + 'ResourceType': 'security-group', + 'Tags': [ + {'Key': 'veraison-deployment', 'Value': deployment_name}, + {'Key': 'Class', 'Value': 'packer'}, + ], + } + ], + ) + + cmd.logger.debug('obtaining public IP address for localhost...') + my_addr = get_public_ip_address() + + cmd.logger.debug('Adding access rule for SSH from localhost...') + authorize_ports_for_address( + cmd.aws, resp['GroupId'], my_addr, [22], cmd.cache.user_tag, + deployment_tags=[ + {'Key': 'veraison-deployment', 'Value': deployment_name}, + {'Key': 'Class', 'Value': 'packer'}, + ], + ) + + +def command_delete_packer_security_group(cmd, deployment_name): + cmd.logger.debug(f'looking for packer group(s) for deployment {deployment_name}...') + resp = cmd.aws.ec2.describe_security_groups( + Filters=[ + {'Name': 'tag:veraison-deployment', 'Values': [deployment_name]}, + {'Name': 'tag:Class', 'Values': ['packer']}, + ] + ) + + group_ids = [sgr['GroupId'] for sgr in resp['SecurityGroups']] + if not group_ids: + cmd.logger.debug(f'no packer group found for deployment {deployment_name}') + return + + for group_id in group_ids: + cmd.logger.debug(f'deleting security group {group_id}...') + cmd.aws.ec2.delete_security_group(GroupId=group_id) + + +def command_update_dynamic_address_rules(cmd, deployment_name, tag, ports): + resp = aws.ec2.describe_security_groups( + Filters=[{ + 'Name': 'tag:veraison-deployment', + 'Values': [deployment_name], + }] + ) + + + group_ids = [sgr['GroupId'] for sgr in resp['SecurityGroups']] + if not group_ids: + cmd.logger.info(f'no groups found for deployment {deployment_name}') + return + + my_addr = get_public_ip_address() + for group_id in group_ids: + revoke_security_group_rules_by_tag(cmd.aws, group_id, tag) + authorize_ports_for_address( + cmd.aws, group_id, my_addr, ports, tag, + deployment_tags=[{'Key': 'veraison-deployment', 'Value': deployment_name}]) + + +def get_stack_instances_info(aws, stack_name): + resp = aws.cf.describe_stack_resources(StackName=stack_name) + + info = {} + + for resource in resp['StackResources']: + if resource['ResourceType'] == 'AWS::EC2::Instance': + instance_id = resource['PhysicalResourceId'] + update_info_with_ec2_instance(aws, info, instance_id) + elif resource['ResourceType'] == 'AWS::RDS::DBInstance': + instance_id = resource['PhysicalResourceId'] + update_info_with_rds_instance(aws, info, instance_id) + + return info + + +def update_info_with_rds_instance(aws, info, instance_id): + resp = aws.rds.describe_db_instances(DBInstanceIdentifier=instance_id) + instance = resp['DBInstances'][0] + + instance_name = None + for tag in instance['TagList']: + if tag['Key'] == 'deployment-instance-name': + instance_name = tag['Value'] + break + + if not instance_name: + # if not explicitly named, assume it's the sole RDS instance for the deployment + instance_name = 'rds' + + info[instance_name] = { + 'id': instance_id, + 'dns_name': instance['Endpoint']['Address'], + 'port': instance['Endpoint']['Port'], + } + + +def update_info_with_ec2_instance(aws, info, instance_id): + resp = aws.ec2.describe_instances(InstanceIds=[instance_id]) + instance = resp['Reservations'][0]['Instances'][0] + + instance_name = None + fallback_name = instance_id + for tag in instance['Tags']: + if tag['Key'] == 'deployment-instance-name': + instance_name = tag['Value'] + break + elif tag['Key'] == 'Name': + fallback_name = tag['Value'] + + if not instance_name: + instance_name = fallback_name + + pub_iface = instance['NetworkInterfaces'][0]['Association'] + + info[instance_name] = { + 'id': instance_id, + 'dns_name': pub_iface["PublicDnsName"], + 'ip_address': pub_iface["PublicIp"], + } + + +def get_ami_id(aws, name): + resp = aws.ec2.describe_images(Owners=['self']) + for image in resp['Images']: + if image['Name'] == name: + return image['ImageId'] + + +def run_in_shell(cmd, should_log): + logger = logging.getLogger('shell') + if should_log: + logger.setLevel(logging.DEBUG) + + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(_run_in_shell_teed(cmd, logger)) + finally: + loop.close() + + +async def _run_in_shell_teed(cmd, logger): + process: Process = await asyncio.create_subprocess_shell( + cmd, stdout=PIPE, stderr=PIPE, cwd=os.getcwd()) + + + stdout_buf, stderr_buf = [], [] + tasks = { + asyncio.Task(process.stdout.readline()): (process.stdout, stdout_buf), + asyncio.Task(process.stderr.readline()): (process.stderr, stderr_buf), + } + + while tasks: + done, _ = await asyncio.wait( + tasks, return_when=asyncio.FIRST_COMPLETED) # pyright: ignore[reportCallIssue] + for future in done: + stream, buf = tasks.pop(future) + line = future.result() + if line: + line = line.decode() + buf.append(line) + logger.debug(line.rstrip('\n')) + tasks[asyncio.Task(stream.readline())] = stream, buf # pyright: ignore[reportOptionalMemberAccess] + + rc = await process.wait() + return rc, ''.join(stdout_buf), ''.join(stderr_buf) + + +def command_get_config(cmd, name): + try: + return cmd.cache[name] + except KeyError: + cmd.fail(f'could not get {name} from cache; has configure command been run?') + + +def command_instantiate_template(cmd, deployment_name, src_path): + with open(src_path) as fh: + instantiated_template_body = envsubst(fh.read()) + + basename = os.path.basename(src_path).removesuffix('.template') + out_path = f'/tmp/{deployment_name}-{basename}' + cmd.logger.debug(f'writing {out_path}') + with open(out_path, 'w') as wfh: + wfh.write(instantiated_template_body) + + return out_path + + +def command_create_image(cmd, args, name, packer_params=None): + if not shutil.which('packer'): + cmd.fail('packer must be installed on the system') + + if not os.path.isfile(args.template): + cmd.fail(f'template {args.template} does not exist') + + full_name = f'{args.deployment_name}-{name}' + cmd.logger.info(f'creating image: {full_name}...') + + command_create_packer_security_group(cmd, args.deployment_name) + + try: + cmd.logger.debug('checking for existing AMI with that name...') + existing_id = get_ami_id(cmd.aws, full_name) + if existing_id: + if not args.force: + cmd.fail(f'image {full_name} already exits (use -f to overwrite)') + cmd.logger.info('removing existing image...') + cmd.aws.ec2.deregister_image(ImageId=existing_id) + + cmd.logger.info('building using packer...') + vpc_id = command_get_config(cmd, 'vpc-id') + subnet_id = command_get_config(cmd, 'subnet-id') + region = command_get_config(cmd, 'region') + + params_dict = { + 'ami_name': full_name, + 'deployment_name': args.deployment_name, + 'instance_type': args.instance_type, + 'region': region, + 'vpc_id': vpc_id, + 'subnet_id': subnet_id, + } + params_dict.update(packer_params or {}) + + params_str = ' '.join(f'-var {k}={v}' for k, v in params_dict.items() if v is not None) + + packer_cmd = f'packer build {params_str} {args.template}' + cmd.logger.debug(packer_cmd) + exit_code, stdout, stderr = run_in_shell(packer_cmd, args.verbose) + if exit_code: + cmd.fail_shell('packer', exit_code, stdout, stderr) + + regex = re.compile(r'AMI: (?Pami-\w+)') + match = regex.search(stdout) + if not match: + cmd.fail('could not find AMI ID in packer output') + + images = cmd.cache.get('images', {}) + images[name] = match.group('id') + cmd.cache['images'] = images + finally: + command_delete_packer_security_group(cmd, args.deployment_name) + + cmd.logger.info('done.') + + +def command_create_stack( + cmd, deployment_name, stack_name, template_path, extra_params, wait_period=60): + cmd.logger.info(f'creating stack {stack_name}...') + + # doing this to be compatible with AWS CLI which specifies the template path as + # file://path/to/template. + url = urlparse(template_path) + cmd.logger.debug(f'template: {url.path}') + with open(url.path) as fh: + template = fh.read() + + vpc_id = command_get_config(cmd, 'vpc-id') + subnet_id = command_get_config(cmd, 'subnet-id') + admin_cidr = command_get_config(cmd, 'admin-cidr') + + params = [ + {'ParameterKey': 'DeploymentName', 'ParameterValue': deployment_name}, + {'ParameterKey': 'VpcId', 'ParameterValue': vpc_id}, + {'ParameterKey': 'SubnetId', 'ParameterValue': subnet_id}, + {'ParameterKey': 'AdminCidr', 'ParameterValue': admin_cidr}, + ] + params.extend(extra_params) + + cmd.logger.debug(f'using params {params}') + resp = cmd.aws.cf.create_stack( + StackName=stack_name, + TemplateBody=template, + Parameters=params, + OnFailure='DELETE', + ) + cmd.logger.debug(f'stack ID: {resp["StackId"]}') + + cmd.logger.info('waiting for the stack creation to complete...') + resp = cmd.aws.cf.describe_stacks(StackName=stack_name) + while resp['Stacks'][0]['StackStatus'] == 'CREATE_IN_PROGRESS': + time.sleep(wait_period) + resp = cmd.aws.cf.describe_stacks(StackName=stack_name) + + stack_status = resp['Stacks'][0]['StackStatus'] + if stack_status == 'CREATE_COMPLETE': + instances = cmd.cache.get('instances', {}) + + cmd.logger.debug(f'getting info for {stack_name}...') + stack_instance_info = get_stack_instances_info(cmd.aws, stack_name) + + cmd.logger.info('instances:') + for name, instance in stack_instance_info.items(): + suffix = instance.get('ip_address', '') + if not suffix: + port = instance.get('port') + if port: + suffix = f'port {port}' + + cmd.logger.info(f' {name}: {instance['dns_name']} ({suffix})') + instances[name] = instance + + cmd.cache['instances'] = instances + + cmd.logger.info('done.') + else: # stack_status != 'CREATE_COMPLETE' + cmd.logger.error(f'creation failed: {stack_status}') + resp = cmd.aws.cf.describe_stack_events(StackName=stack_name) + + for event in resp['StackEvents']: + if event['ResourceStatus'] == 'CREATE_IN_PROGRESS': + break + status = event['ResourceStatus'] + reason = event.get("ResourceStatusReason", '') + cmd.logger.error(f'{status} {reason}') + + cmd.logger.info('waiting for the rollback to complete...') + resp = cmd.aws.cf.describe_stacks(StackName=stack_name) + while resp['Stacks'][0]['StackStatus'] == 'DELETE_IN_PROGRESS': + time.sleep(wait_period) + resp = cmd.aws.cf.describe_stacks(StackName=stack_name) + + cmd.fail(f'could not create stack {stack_name}') + + +def command_connect(cmd, instance_name, user='ubuntu', should_log=True): + instance = cmd.cache.get('instances', {}).get(instance_name) + if instance is None: + cmd.fail(f'could not find instance {instance_name} in cache') + + key_path = cmd.cache.get('key', {}).get('path') + if not key_path: + cmd.fail('couild not find key in cache') + + return fabric.Connection( + instance['dns_name'], + user=user, + connect_kwargs={ + 'key_filename': key_path, + }, + ) + + +def con_transfer_file(con, src, dest, owner=None, preserve_mode=True): + temp_dest = randomword(32) + os.path.basename(dest) + con.put(src, remote=temp_dest, preserve_mode=preserve_mode) + con.sudo(f'mv ~/{temp_dest} {dest}') + if owner: + con.sudo(f'chown -R {owner} {dest}') + + +def create_ec_key(key_path): + key = ec.generate_private_key(curve=ec.SECP256R1()) + + with open(key_path, 'wb') as wfh: + wfh.write(key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + )) + + os.chmod(key_path, stat.S_IRUSR | stat.S_IWUSR) + + return key + + +def create_ca_cert(cert_path, key_path): + key = create_ec_key(key_path) + + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Veraison'), + ]) + now = datetime.now(timezone.utc) + + cert = ( + x509.CertificateBuilder(public_key=key.public_key()) + .subject_name(subject) + .issuer_name(issuer) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now + timedelta(days=3650)) + .add_extension( + x509.BasicConstraints(ca=True, path_length=None), + critical=True, + ) + .add_extension( + x509.KeyUsage( + digital_signature=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(key.public_key()), + critical=False, + ) + ).sign(key, hashes.SHA256()) + + with open(cert_path, 'wb') as wfh: + wfh.write(cert.public_bytes(serialization.Encoding.PEM)) + + return cert, key + + +def create_service_cert(service_name, service_host, dest_path, ca_cert, ca_key): + cert_path = os.path.join(dest_path, f'{service_name}.crt') + key_path = os.path.join(dest_path, f'{service_name}.key') + + key = create_ec_key(key_path) + + subject = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, service_host), + ]) + now = datetime.now(timezone.utc) + + cert = ( + x509.CertificateBuilder(public_key=key.public_key()) + .subject_name(subject) + .issuer_name(ca_cert.subject) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now + timedelta(days=3650)) + .add_extension( + x509.SubjectAlternativeName([ + x509.DNSName(service_host), + x509.DNSName('localhost'), + ]), + critical=False, + ) + .add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ) + .add_extension( + x509.ExtendedKeyUsage([ + x509.ExtendedKeyUsageOID.CLIENT_AUTH, # pyright: ignore + x509.ExtendedKeyUsageOID.SERVER_AUTH, # pyright: ignore + ]), + critical=False, + ).add_extension( + x509.SubjectKeyIdentifier.from_public_key(key.public_key()), + critical=False, + ) + .add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value, + ), + critical=False, + ) + ).sign(ca_key, hashes.SHA256()) + + with open(cert_path, 'wb') as wfh: + wfh.write(cert.public_bytes(serialization.Encoding.PEM)) + + return cert_path, key_path + + +class DeploymentCache: + + @property + def dir(self): + return os.path.dirname(self.path) + + @property + def certs_dir(self): + return os.path.join(self.dir, 'certs') + + @property + def ca_cert_path(self): + return os.path.join(self.certs_dir, 'rootCA.crt') + + @property + def ca_key_path(self): + return os.path.join(self.certs_dir, 'rootCA.key') + + @property + def user_tag(self): + return self.db.get('user_tag', self.default_user_tag) + + def __init__(self, name, cache_dir=None): + if not name: + raise ValueError('name cannot be empty') + self.name = name + if cache_dir is None: + cache_dir = xdg.BaseDirectory.save_data_path('veraison/aws') + self.path = os.path.join(cache_dir, f'{self.name}.db') + self.db = SqliteDict(self.path) + self.default_user_tag = f'{socket.gethostname()}-{getpass.getuser()}' + + def get(self, key, default=None): + return self.db.get(key, default) + + def as_dict(self): + return {k: v for k, v in self.db.items()} + + def close(self): + self.db.close() + + def __getitem__(self, key): + return self.db[key] + + def __setitem__(self, key, value): + self.db[key] = value + self.db.commit() + + def __delitem__(self, key): + try: + del self.db[key] + self.db.commit() + except KeyError: + pass + + +class StoreIntList(argparse.Action): + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, [int(v) for v in values.split(',')]) # pyright: ignore + + +class LogFormatter(logging.Formatter): + + fmt = f'{{}}%(asctime)s %(name)s %(levelname)s{COLOR_RESET}: %(message)s' + + level_formats = { + logging.DEBUG: fmt.format(COLOR_DARK_GREY), + logging.INFO: fmt.format(COLOR_MEDIUM_GREY), + logging.WARNING: fmt.format(COLOR_YELLOW), + logging.ERROR: fmt.format(COLOR_RED), + logging.CRITICAL: fmt.format(COLOR_BOLD_RED), + } + + def format(self, record): + log_fmt = self.level_formats.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +class BaseCommand: + + name = None + desc = None + aliases = [] + + def __init__(self, aws): + self.aws = aws + self.logger = logging.getLogger(self.name) + self.cache = None + + def register(self, subparsers): + parser = subparsers.add_parser(self.name, help=self.desc, aliases=self.aliases) + self.update_arguments(parser) + + def execute(self, args): + if args.verbose and args.quiet: + self.fail('only one of -v/--verbose or -q/--quiet may be specfied at a time') + if args.verbose: + self.logger.setLevel(logging.DEBUG) + elif args.quiet: + self.logger.setLevel(logging.WARNING) + + self.cache = DeploymentCache(args.deployment_name, args.cache_dir) + try: + self.run(args) + finally: + self.cache.close() + + def fail(self, message): + self.logger.error(message) + raise RuntimeError(f'command {self.name} failed.') + + def fail_shell(self, command, exit_code, stdout, stderr): + stdout_file = f'/tmp/{args.deployment_name}-{command}-failure.stdout' + with open(stdout_file, 'w') as wfh: + wfh.write(stdout) + + stderr_file = f'/tmp/{args.deployment_name}-{command}-failure.stderr' + with open(stderr_file, 'w') as wfh: + wfh.write(stderr) + + self.fail(f'{command} failed with {exit_code}' + f'\n\tSTDOUT is in {stdout_file}\n\tSTDERR is in {stderr_file}') + + def update_arguments(self, parser): + pass + + def run(self, *args, **kwargs): + raise NotImplementedError() + + +class CreateCombinedStackCommand(BaseCommand): + + name = 'create-combined-stack' + desc = 'create deployment\'s cloudformation stack running services on one instance' + + def update_arguments(self, parser): + parser.add_argument('-k', '--key-name') + + def run(self, args): + stack_name = f'{args.deployment_name}-combined' + template_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/stack-combined.yaml', + )) + + rds_subnets = command_get_config(self, 'rds-subnet-ids') + + key_name = self._get_key_name(args) + + combined_image = self.cache.get('images', {}).get('combined') + if not combined_image: + self.fail('could not find combined image in cache; ' + 'please run create-combined-image command') + + keycloak_image = self.cache.get('images', {}).get('keycloak') + if not keycloak_image: + self.fail('could not find keycloak image in cache; ' + 'please run create-keycloak-image command') + + self.logger.debug('looking up subnet CIDR...') + subnet_id = command_get_config(cmd, 'subnet-id') + resp = self.aws.ec2.describe_subnets(SubnetIds=[subnet_id]) + subnet_cidr = resp['Subnets'][0]['CidrBlock'] + + extra_params = [ + {'ParameterKey': 'KeyName', 'ParameterValue': key_name}, + {'ParameterKey': 'CombinedImage', 'ParameterValue': combined_image}, + {'ParameterKey': 'KeycloakImage', 'ParameterValue': keycloak_image}, + {'ParameterKey': 'SubnetCidr', 'ParameterValue': subnet_cidr}, + {'ParameterKey': 'RdsSubnets', 'ParameterValue': ','.join(rds_subnets)}, # pyright: ignore + ] + + command_create_stack(cmd, args.deployment_name, stack_name, + template_path, extra_params, args.wait_period) + + def _get_key_name(self, args): + if args.key_name: + return args.key_name + + key_info = self.cache.get('key') + if key_info: + return key_info['name'] + + self.fail('could not find key name (specify with --key-name or run ' + 'create-key-pair command)') + + +class DeleteStackCommand(BaseCommand): + + name = 'delete-stack' + desc = 'delete a previously created stack' + + def update_arguments(self, parser): + parser.add_argument('name') + + def run(self, args): + stack_name = f'{args.deployment_name}-{args.name}' + self.logger.info(f'deleting stack {stack_name}...') + + self.logger.debug(f'getting {stack_name} instances...') + stack_instances = get_stack_instances_info(self.aws, stack_name) + + self.aws.cf.delete_stack(StackName=stack_name) + try: + self.logger.debug('waiting for the stack deletion to complete...') + resp = self.aws.cf.describe_stacks(StackName=stack_name) + while resp['Stacks'][0]['StackStatus'] == 'DELETE_IN_PROGRESS': + time.sleep(args.wait_period) + resp = self.aws.cf.describe_stacks(StackName=stack_name) + except ClientError as e: + if 'does not exist' not in str(e): + raise e + + self.logger.debug('updating instances in cache...') + instances = self.cache.get('instances', {}) + for instance_name in stack_instances: + try: + del instances[instance_name] + except KeyError: + pass + self.cache['instances'] = instances + + self.logger.debug('removing RDS settings from cache...') + del self.cache['rds'] + + self.logger.info('done.') + + +class DeleteCertsCommand(BaseCommand): + + name = 'delete-certs' + desc = 'delete previously created service certs' + + def run(self, args): + self.logger.info('deleting service certificates...') + certs = self.cache.get('certs', {}) + if not certs: + self.logger.debug('no service certs found in cache') + + for cname, cpaths in certs.items(): + self.logger.debug(f'deleting {cname}...') + os.remove(cpaths['cert']) + os.remove(cpaths['key']) + + self.cache['certs'] = {} + self.logger.info('done.') + + +class UpdateSecurityGroupsCommand(BaseCommand): + + name = 'update-security-groups' + desc = 'update security group(s) in deployment with current host\'s IP address' + + def update_arguments(self, parser): + parser.add_argument('-p', '--ports', action=StoreIntList, + default=[11111, 8888, 8088, 8080, 5432, 22]) + + def run(self, args): + self.logger.info('updating deployment security groups with IP address for this host...') + + try: + command_update_dynamic_address_rules( + self, args.deployment_name, self.cache.user_tag, args.ports) + except Exception as e: + self.fail(e) + + self.logger.info('done.') + + +class CreateCombinedImageCommand(BaseCommand): + + name = 'create-combined-image' + desc = 'create IMA image for the Veraison services EC2 instance' + + def update_arguments(self, parser): + parser.add_argument('-D', '--deb') + parser.add_argument( + '-t', '--template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/image-combined.pkr.hcl', + )), + ) + parser.add_argument('-T', '--instance-type') + + def run(self, args): + deb_path = args.deb or self.cache['deb'] + if not os.path.isfile(deb_path): + self.fail(f'{deb_path} does not exist') + self.cache['deb'] = deb_path + + command_create_image(self, args, 'combined', {'deb': deb_path}) + + +class ConfigureCommand(BaseCommand): + + name = 'configure' + desc = 'configure deployment parameters' + + default_provisioning_user = 'veraison-provisioner' + default_provisioning_password = 'veraison' + default_management_user = 'veraison-manager' + default_management_password = 'veraison' + default_client_id = 'veraison-client' + default_client_secret = 'YifmabB4cVSPPtFLAmHfq7wKaEHQn10Z' + + def update_arguments(self, parser): + parser.add_argument('-a', '--admin-cidr') + parser.add_argument('-i', '--init', action='store_true', + help='initialize config not explicitly specified') + parser.add_argument('-r', '--region') + parser.add_argument('-s', '--subnet-id') + parser.add_argument('-v', '--vpc-id') + + parser.add_argument('-R', '--rds-subnet-ids', action='append') + + parser.add_argument('-p', '--provisioning-user' , default=None) + parser.add_argument('-P', '--provisioning-password' , default=None) + parser.add_argument('-m', '--management-user' , default=None) + parser.add_argument('-M', '--management-password' , default=None) + parser.add_argument('-C', '--client-id' , default=None) + parser.add_argument('-S', '--client-secret' , default=None) + + def run(self, args): + self._configure_vpc_id(args) + self._configure_subnet_id(args) + self._configure_region(args) + self._conifigure_admin_cidr(args) + self._conifigure_client_settings(args) + self._conifigure_rds_subnet_ids(args) + + def _conifigure_admin_cidr(self, args): + if args.admin_cidr: + self.cache['admin-cidr'] = args.admin_cidr + elif args.init: + self.logger.warning('setting admin CIDR to 0.0.0.0/0; this is not recommended: ' + 're-run with --admin-cidr option') + self.cache['admin-cidr'] = '0.0.0.0/0' + + def _conifigure_client_settings(self, args): + attrs = [ + 'provisioning_user', + 'provisioning_password', + 'management_user', + 'management_password', + 'client_id', + 'client_secret', + ] + + if args.init: + for attr in attrs: + default = getattr(self.__class__, f'default_{attr}') + setattr(args, attr, getattr(args, attr) or default) + + client_config = self.cache.get('client_config', {}) + + for attr in attrs: + new_val = getattr(args, attr) + if new_val: + client_config[attr] = new_val + + self.cache['client_config'] = client_config + + def _configure_vpc_id(self, args): + if args.vpc_id: + self.logger.debug(f'writing {args.vpc_id} to cache') + self.cache['vpc-id'] = args.vpc_id + return + elif not args.init and self.cache.get('vpc-id'): + return # already set and not re-initializing + + self.logger.debug('no VPC ID specified; trying to identify from account...') + resp = self.aws.ec2.describe_vpcs( + Filters=[{ + 'Name': 'state', + 'Values': ['available'], + }] + ) + if len(resp['Vpcs']) == 1: + vpc_id = resp['Vpcs'][0]['VpcId'] + self.cache['vpc-id'] = vpc_id + elif len(resp['Vpcs']) > 1: + vpc_ids = ', '.join(vpc['VpcId'] for vpc in resp['Vpcs']) + self.fail(f'multiple VPCs found: {vpc_ids}; use --vpc-id to specify ' + 'which one should be used') + else: + self.fail('no VPCs found in the account') + + def _configure_subnet_id(self, args): + if args.subnet_id: + self.logger.debug(f'writing {args.subnet_id} to cache') + self.cache['subnet-id'] = args.subnet_id + return + elif not args.init and self.cache.get('subnet-id'): + return # already set and not re-initializing + + self.logger.debug('no subnet ID specified; trying to identify from account...') + resp = self.aws.ec2.describe_subnets( + Filters=[{ + 'Name': 'state', + 'Values': ['available'], + }] + ) + if len(resp['Subnets']) == 1: + subnet_id = resp['Subnets'][0]['SubnetId'] + self.cache['subnet-id'] = subnet_id + elif len(resp['Subnets']) > 1: + subnet_ids = ', '.join(subnet['SubnetId'] for subnet in resp['Subnets']) + self.fail(f'multiple subnets found: {subnet_ids}; use --subnet-id to specify ' + 'which one should be used') + else: + self.fail('no subnets found in the account') + + def _configure_region(self, args): + if args.region: + self.logger.debug(f'writing {args.region} to cache') + self.cache['region'] = args.region + return + elif not args.init and self.cache.get('region'): + return # already set and not re-initializing + + subnet_id = command_get_config(self, 'subnet-id') + + resp = self.aws.ec2.describe_subnets(SubnetIds=[subnet_id]) + zone_id = resp['Subnets'][0]['AvailabilityZoneId'] + + resp = self.aws.ec2.describe_availability_zones(ZoneIds=[zone_id]) + region = resp['AvailabilityZones'][0]['RegionName'] + + self.cache['region'] = region + + def _conifigure_rds_subnet_ids(self, args): + if args.init and not args.rds_subnet_ids: + self.fail('-R/--rds-subnet-ids must be specified') + + subnet_ids = [] + for entry in args.rds_subnet_ids: + subnet_ids.extend(entry.split(',')) + + if subnet_ids: + self.cache['rds-subnet-ids'] = subnet_ids + + +class CreateKeycloakImageCommand(BaseCommand): + + name = 'create-keycloak-image' + desc = 'create IMA image for the Keycloak EC2 instance' + + def update_arguments(self, parser): + parser.add_argument( + '-c', '--keycloak-config-template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/keycloak.conf.template', + )), + ) + parser.add_argument( + '-s', '--keycloak-service-template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/keycloak.service.template', + )), + ) + parser.add_argument( + '-t', '--template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/image-keycloak.pkr.hcl', + )), + ) + parser.add_argument('-T', '--instance-type') + + def run(self, args): + if not os.environ.get('KEYCLOAK_ADMIN_PASSWORD'): + self.logger.debug('generating admin password for Keycloak and writing into cache...') + # note: not using punctuation in the initial password, as it will be passed through + # multiple shells, environments, and tools, and we don't want to + # warry about correctrly escaping everything at every stage. Using a longer string + # to compensate. + password = randomword(40) + os.environ['KEYCLOAK_ADMIN_PASSWORD'] = password + self.cache['kc_password'] = password + + conf_path = command_instantiate_template( + self, args.deployment_name, args.keycloak_config_template) + service_path = command_instantiate_template( + self, args.deployment_name, args.keycloak_service_template) + command_create_image(self, args, 'keycloak', + {'conf_path': conf_path, 'service_path': service_path}) + + +class DeleteImageCommand(BaseCommand): + + name = 'delete-image' + desc = 'delete IMA image for the Veraison services EC2 instance' + + def update_arguments(self, parser): + parser.add_argument('name') + + def run(self, args): + images = self.cache.get('images', {}) + iid = images.get(args.name) + if iid is None: + self.fail(f'no entry for image {args.name} found in the deployment cache') + + self.logger.info(f'deleting image {args.name} ({iid})...') + self.aws.ec2.deregister_image(ImageId=iid) + + self.logger.debug(f'removing image {args.name} from cache') + del images[args.name] + self.cache['images'] = images + + self.logger.info('done.') + + +class CreateKeyPairCommand(BaseCommand): + + name = 'create-key-pair' + desc = 'create a key pair that will be used for SSH access to the deployment\'s instances' + + def update_arguments(self, parser): + parser.add_argument('-n', '--key-name') + parser.add_argument('-t', '--key-type', choices=['rsa', 'ed25519'], default='rsa') + + def run(self, args): + key_info = self.cache.get('key') + if key_info: + self.fail(f'key pair for {args.deployment_name} already exits: ' + f'{key_info['name']} ({key_info['id']})') + + name = args.key_name or os.getenv('VERAISON_AWS_KEY') or args.deployment_name + + self.logger.info(f'creating key pair {name} for {args.deployment_name}...') + resp = aws.ec2.create_key_pair( + KeyName=name, + KeyType=args.key_type, + KeyFormat='pem', + TagSpecifications=[ + { + 'ResourceType': 'key-pair', + 'Tags': [ + {'Key': 'veraison-deployment', 'Value': args.deployment_name}, + ], + } + ], + ) + + path = os.path.join(self.cache.dir, f'{name}_{args.key_type}') + self.logger.info(f'writing private key to {path}') + with open(path, 'w') as wfh: + wfh.write(resp['KeyMaterial']) + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) + + self.cache['key'] ={ + 'name': name, + 'id': resp['KeyPairId'], + 'fingerprint': resp['KeyFingerprint'], + 'path': path, + } + + self.logger.info('done.') + + +class DeleteKeyPairCommand(BaseCommand): + + name = 'delete-key-pair' + desc = 'create a key pair that will be used for SSH access to the deployment\'s instances' + + def run(self, args): + self.logger.info(f'deleting key pair for {args.deployment_name}...') + key_info = self.cache.get('key') + if key_info: + if os.path.isfile(key_info['path']): + self.logger.debug(f'deleting {key_info['path']}') + os.remove(key_info['path']) + else: + self.logger.debug(f'{key_info['path']} not found (already deleted?)') + self.logger.debug(f'deleting AWS key pair {key_info['name']} ({key_info['id']})') + self.aws.ec2.delete_key_pair(KeyPairId=key_info['id']) + del self.cache['key'] + self.logger.info('done.') + else: + self.logger.debug('no key info cached; checking VERAISON_AWS_KEY') + key_name = os.getenv('VERAISON_AWS_KEY') + if key_name: + self.logger.debug(f'deleting AWS key pair {key_name}') + self.aws.ec2.delete_key_pair(KeyName=key_name) + else: + self.logger.debug('VERAISON_AWS_KEY not specified; search for key ' + f'tagged with {args.deployment_name}') + resp = self.aws.ec2.describe_key_pairs( + Filters=[{ + 'Name': 'tag:veraison-deployment', + 'Values': [ + args.deployment_name, + ], + }], + ) + + if len(resp['KeyPairs']) == 1: + name = resp['KeyPairs'][0]['KeyName'] + kid = resp['KeyPairs'][0]['KeyPairId'] + self.logger.debug(f'deleting AWS key pair {name} ({kid})') + self.aws.ec2.delete_key_pair(KeyPairId=kid) + else: + if len(resp['KeyPairs']) > 1: + names = ', '.join([kp['KeyName'] for kp in resp['KeyPairs']]) + self.logger.error(f'multiple key pairs for {args.deployment_name} found ' + f'({names}). Specify key name using VERAISON_AWS_KEY') + else: + self.logger.error(f'no key pairs found for {args.deployment_name}') + + self.fail(f'could not delete key pair for {args.deployment_name}') + + self.logger.info('done. (local files not touched)') + + +class CreateDebCommand(BaseCommand): + + name = 'create-deb' + desc = 'create the Veraison Debian package' + + def update_arguments(self, parser): + parser.add_argument( + '-s', '--veraison-src', + help='path to Veraison services source; if not specified, ' + 'it will be guess based on this script\'s location', + ) + parser.add_argument( + '-w', '--work-dir', default='/tmp', + help='this will be used as the working directory when creating the .deb. ' + 'Upon completion, t will contain the intermediate artifacts.', + ) + + def run(self, args): + src_root = args.veraison_src + if src_root is None: + src_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')) + + script = os.path.join(src_root, 'deployments/debian/deployment.sh') + if not os.path.isfile(script): + self.fail(f'script {script} does not exist') + + self.logger.info(f'creating Debian package under {args.work_dir}...') + create_deb_cmd = f'{script} create-deb {args.work_dir}' + self.logger.debug(create_deb_cmd) + exit_code, stdout, stderr = run_in_shell(create_deb_cmd, args.verbose) + if exit_code: + self.fail_shell('deb creation', exit_code, stdout, stderr) + + regex = re.compile(r"building package 'veraison' in '(?P[^']+)'") + match = regex.search(stdout) + if not match: + self.fail(f'could not find deb path in script output') + + deb_path = match.group('deb_path') + dest_path = os.path.join(args.cache_dir, os.path.basename(deb_path)) + self.logger.debug(f'moving {deb_path} to {dest_path}') + shutil.move(deb_path, dest_path) + + self.logger.debug('updating deployment cache') + self.cache['deb'] = dest_path + + self.logger.info(f'created {dest_path}') + + self.logger.info('done.') + + +class DeleteDebCommand(BaseCommand): + + name = 'delete-deb' + desc = 'delete perviously created Debian package' + + def run(self, args): + self.logger.info('deleting cached Debian package...') + + deb_path = self.cache.get('deb') + if not deb_path: + self.fail('could not find deb in cache') + + self.logger.debug(f'removing deb {deb_path}') + os.remove(deb_path) + del self.cache['deb'] + + self.logger.info('done.') + + +class CreateDebugStack(BaseCommand): + name = 'create-debug-stack' + desc = 'create a stack with a single instance that can be used for testing' + + def update_arguments(self, parser): + parser.add_argument('-k', '--key-name') + parser.add_argument('-i', '--image-id', default='ami-02c6977f57c0816de') + + def run(self, args): + stack_name = f'{args.deployment_name}-debug' + template_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/stack-debug.yaml', + )) + key_name = self._get_key_name(args) + extra_params = [ + {'ParameterKey': 'KeyName', 'ParameterValue': key_name}, + {'ParameterKey': 'InstanceImage', 'ParameterValue': args.image_id}, + ] + + command_create_stack(cmd, args.deployment_name, stack_name, + template_path, extra_params, args.wait_period) + + def _get_key_name(self, args): + if args.key_name: + return args.key_name + + key_info = self.cache.get('key') + if key_info: + return key_info['name'] + + self.fail('could not find key name (specify with --key-name or run ' + 'create-key-pair command)') + + +class ShellCommand(BaseCommand): + + name = 'shell' + desc = 'start a shell on a deployment instance' + + def update_arguments(self, parser): + parser.add_argument('instance', choices=['combined', 'keycloak', 'test']) + parser.add_argument('-k', '--ssh-key') + parser.add_argument('-u', '--user', default='ubuntu') + parser.add_argument('-s', '--server-alive-interval', type=int, default=60) + + def run(self, args): + if not shutil.which('ssh'): + self.fail('ssh does not appear to be installed on the system') + + instance = self.cache.get('instances', {}).get(args.instance) + if not instance: + self.fail(f'{instance} instance not in cache; has the correct stack been created?') + + if args.ssh_key: + key = args.ssh_key + else: + key = self.cache.get('key', {}).get('path') + + if not key: + self.fail(f'key not found in cache specify with --ssh-key') + + ssh_opts = '-o StrictHostKeyChecking=no' + if args.server_alive_interval: + ssh_opts += f' -o ServerAliveInterval={args.server_alive_interval}' + + ssh_cmd = f'ssh {ssh_opts} -i {key} ubuntu@{instance['dns_name']}' + self.logger.debug(ssh_cmd) + os.system(ssh_cmd) + + +PGSQL_SETUP = ''' +CREATE TABLE IF NOT EXISTS endorsements ( + kv_key TEXT NOT NULL, + kv_val TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS trust_anchors ( + kv_key TEXT NOT NULL, + kv_val TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS policies ( + kv_key TEXT NOT NULL, + kv_val TEXT NOT NULL +); + +CREATE INDEX ON endorsements(kv_key); +CREATE INDEX ON trust_anchors(kv_key); +CREATE INDEX ON policies(kv_key); +''' + + +class SetupRdsCommand(BaseCommand): + + name = 'setup-rds' + desc = 'setup the RDS instance for use as a K-V store for the services' + + def update_arguments(self, parser): + parser.add_argument('-d', '--dbname', default='veraison') + parser.add_argument('-U', '--user', default='veraison') + parser.add_argument('-p', '--password', default='veraison') + + def run(self, args): + if not shutil.which('psql'): + self.fail('psql must be installed on the system') + + rds = self.cache.get('instances', {}).get('rds') + if not rds: + self.fail('could not not find RDS instance in cache; ' + 'has the associated stack been created?') + + self.logger.info('connecting to RDS instance...') + con_line = (f'host={rds['dns_name']} port={rds['port']} dbname={args.dbname} ' + f'user={args.user} password={args.password}') + + self.logger.debug(f'connection settings: {con_line}') + with psycopg2.connect(con_line) as con: + with con.cursor() as cur: + self.logger.info('setting up K-V store...') + for sql_statement in PGSQL_SETUP.split(';'): + sql_statement = sql_statement.strip() + if not sql_statement: + continue + self.logger.debug(sql_statement) + cur.execute(sql_statement) + + self.logger.debug('updating cache with DB connection settings...') + self.cache['rds'] = { + 'RDS_HOST': rds['dns_name'], + 'RDS_PORT': rds['port'], + 'RDS_DBNAME': args.dbname, + 'RDS_USER': args.user, + 'RDS_PASSWORD': args.user, + } + + self.logger.info('done.') + + +class DbShellCommand(BaseCommand): + + name = 'dbshell' + desc = 'start a database shell on a deployment\'s RDS instance' + + def update_arguments(self, parser): + parser.add_argument('-d', '--dbname') + parser.add_argument('-U', '--user', default='veraison') + + def run(self, args): + if not shutil.which('psql'): + self.fail('psql must be installed on the system') + + rds = self.cache.get('instances', {}).get('rds') + if not rds: + self.fail('could not not find RDS instance in cache; ' + 'has the associated stack been created?') + + opts = { + '--host': rds['dns_name'], + '--port': rds['port'], + '--user': args.user, + '--dbname': args.dbname, + } + + opt_string = ' '.join(f'{k}={v}' for k, v in opts.items()) + psql_cmd = f'psql {opt_string}' + self.logger.debug(psql_cmd) + os.system(psql_cmd) + + +class PushCommand(BaseCommand): + + name = 'push' + desc = 'copy a file/directory from the host to a deployment instance' + + def update_arguments(self, parser): + parser.add_argument('-i', '--instance' , default='combined', + choices=['combined', 'keycloak']) + parser.add_argument('-P', '--no-preserve-mode', action='store_true') + parser.add_argument('-s', '--sudo', action='store_true') + parser.add_argument('-o', '--owner', help='implies --sudo') + parser.add_argument('src') + parser.add_argument('dest') + + def run(self, args): + if args.owner: + args.sudo = True + + with command_connect(self, args.instance) as con: + if args.sudo: + con_transfer_file( + args.src, args.dest, + args.owner, preserve_mode=(not args.no_preserve_mode)) + else: + con.put(args.src, remote=args.dest, preserve_mode=(not args.no_preserve_mode)) + + +class PullCommand(BaseCommand): + + name = 'pull' + desc = 'copy a file/directory from a deployment instance to the host' + + def update_arguments(self, parser): + parser.add_argument('-i', '--instance' , default='combined', + choices=['combined', 'keycloak']) + parser.add_argument('-P', '--no-preserve-mode', action='store_true') + parser.add_argument('src') + parser.add_argument('dest') + + def run(self, args): + self.logger.debug(f'copying {args.src} on the host to {args.dest} ' + f'on {args.instance} instance') + with command_connect(self, args.instance) as con: + con.get(args.src, local=args.dest, preserve_mode=(not args.no_preserve_mode)) + + +class CreateClientConfigCommand(BaseCommand): + + name = 'create-client-config' + desc = ''' + create configuration for Veraison clients to access the deployment + ''' + all_clients = ['cocli', 'evcli', 'pocli'] + + def update_arguments(self, parser): + parser.add_argument('-c', '--client', + action='append', dest='clients', choices=self.all_clients) + parser.add_argument('-o', '--output-dir', default=xdg.BaseDirectory.xdg_config_home) + + def run(self, args): + self.logger.info('creating Veraison client config(s)...') + if not os.path.isfile(self.cache.ca_cert_path): + self.fail('could not find ca-cert in cache; has create-certs been called?') + + client_config = self.cache.get('client_config') + if not client_config: + self.fail('client config not found; run configure command with appropriate options') + + with command_connect(self, 'combined') as con: + + self.logger.debug(f'getting services config from {con.host}...') + res = con.run( + 'cat /opt/veraison/config/services/config.yaml', + echo=False, hide=True, + ) + if res.exited != 0: + self.fail(f'could not read services config; got {res.exited}: {res.stderr}') + srv_cfg = yaml.safe_load(res.stdout) + + kc_host = srv_cfg.get('auth', {}).get('host') + kc_port = srv_cfg.get('auth', {}).get('port') + if not (kc_host and kc_port): + self.fail('keycloak host/port not found in services config; ' + 'has auth been configured?') + + clients = args.clients or self.all_clients + for client in clients: + self.logger.info(f'generating {client} config...') + outdir = os.path.join(args.output_dir, client) + if not os.path.isdir(outdir): + self.logger.debug(f'creating {outdir}') + os.makedirs(outdir) + + config = getattr(self, f'get_{client}_config')( + srv_cfg, client_config, con.host, kc_host, kc_port, + ) + + outfile = os.path.join(outdir, 'config.yaml') + self.logger.debug(f'writing {outfile}') + with open(outfile, 'w') as wfh: + yaml.dump(config, wfh) + + self.cache['client_config_dir'] = args.output_dir + self.logger.info('done.') + + def get_cocli_config(self, srv_cfg, cli_cfg, host, kc_host, kc_port): + port = int(srv_cfg['provisioning']['listen-addr'].split(':')[1]) + return { + 'ca_cert': self.cache.ca_cert_path, + 'api_server': f'https://{host}:{port}/endorsement-provisioning/v1/submit', + 'auth': 'oauth2', + 'username': cli_cfg['provisioning_user'], + 'password': cli_cfg['provisioning_password'], + 'client_id': cli_cfg['client_id'], + 'client_secret': cli_cfg['client_secret'], + 'token_url': f'https://{kc_host}:{kc_port}' + '/realms/veraison/protocol/openid-connect/token', + } + + def get_evcli_config(self, srv_cfg, cli_cfg, host, kc_host, kc_port): + port = int(srv_cfg['verification']['listen-addr'].split(':')[1]) + return { + 'ca_cert': self.cache.ca_cert_path, + 'api_server': f'https://{host}:{port}/challenge-response/v1/newSession', + } + + def get_pocli_config(self, srv_cfg, cli_cfg, host, kc_host, kc_port): + port = int(srv_cfg['management']['listen-addr'].split(':')[1]) + return { + 'ca_cert': self.cache.ca_cert_path, + 'tls': True, + 'host': host, + 'port': port, + 'auth': 'oauth2', + 'username': cli_cfg['management_user'], + 'password': cli_cfg['management_password'], + 'client_id': cli_cfg['client_id'], + 'client_secret': cli_cfg['client_secret'], + 'token_url': f'https://{kc_host}:{kc_port}' + '/realms/veraison/protocol/openid-connect/token', + } + + +class CacheCommand(BaseCommand): + + name = 'cache' + desc = 'show cached info for the deployment' + + def update_arguments(self, parser): + parser.add_argument('-q', '--query') + + def run(self, args): + if args.query: + parts = args.query.split('.') + entry = self.cache + path = '' + + for part in parts[:len(parts)-1]: + entry = self._access_member(entry, part, path) + path = path + '.' + part + + val = self._access_member(entry, parts.pop(), path) + if val is not None: + sys.stdout.write(val) + else: + print(f'deployment: {args.deployment_name}') + pprint.pp(self.cache.as_dict()) + + def _access_member(self, entry, part, path): + try: # if part is an int, assume list entry + idx = int(part) + if (len(entry)-1) > idx: # pyright: ignore[reportArgumentType] + if path: + self.fail(f'index {idx} does not exist for cache entry "{path}"') + else: + self.fail(f'index {idx} does not exist in cache') + return entry[idx] + except ValueError: # part not an int, assume dict entry + return entry[part] + + +class CheckStoresCommand(BaseCommand): + name = 'check-stores' + desc = 'output the contents of deployment\'s sqlite3 stores' + aliases = ['stores'] + + def run(self, args): + rds_settings = self.cache.get('rds') + if not rds_settings: + self.fail('could not find RDS settings in cache; run setup-rds command') + + con_line = (f'host={rds_settings['RDS_HOST']} port={rds_settings['RDS_PORT']} ' + f'dbname={rds_settings['RDS_DBNAME']} ' + f'user={rds_settings['RDS_USER']} password={rds_settings['RDS_PASSWORD']}') + + self.logger.debug(f'RDS connection settings: {con_line}') + with psycopg2.connect(con_line) as con: + with con.cursor() as cur: + print(f'{COLOR_GREEN}TRUST ANCHORS:\n--------------{COLOR_RESET}') + cur.execute('SELECT * FROM trust_anchors') + for key, value in cur.fetchall(): + print(key) + parsed = json.loads(value) + print(json.dumps(parsed, indent=4, sort_keys=True)) + print() + + print(f'{COLOR_GREEN}ENDORSEMENTS:\n-------------{COLOR_RESET}') + cur.execute('SELECT * FROM endorsements') + for key, value in cur.fetchall(): + print(key) + parsed = json.loads(value) + print(json.dumps(parsed, indent=4, sort_keys=True)) + print() + + print(f'{COLOR_GREEN}POLICIES:\n---------{COLOR_RESET}') + cur.execute('SELECT * FROM policies') + for key, value in cur.fetchall(): + print(key) + print(f'----\n{value}----\n') + print() + + +class StatusCommand(BaseCommand): + + name = 'status' + desc = 'show status of the deployment' + + def run(self, args): + print(f'deployment: {args.deployment_name}') + vpc_id = self.cache.get('vpc-id', f'{COLOR_DARK_GREY}none{COLOR_RESET}') + print(f' vpc: {vpc_id}') + subnet_id = self.cache.get('subnet-id', f'{COLOR_DARK_GREY}none{COLOR_RESET}') + print(f' subnet: {subnet_id}') + + instance = self.cache.get('instances', {}).get('combined') + if not instance: + print(f' instance: {COLOR_DARK_GREY}not created{COLOR_RESET}') + return + + host = instance['dns_name'] + addr = instance['ip_address'] + + try: + with command_connect(self, 'combined') as con: + res = con.run('/opt/veraison/bin/veraison -s status', echo=False, hide=True) + print(f' instance: {host} ({addr}) {COLOR_GREEN}up{COLOR_RESET}') + print(f' services:') + print(res.stdout.rstrip('\n')) + except Exception as e: + self.logger.debug(f'error connecting to instance: {e}') + print(f' instance: {host} ({addr}) {COLOR_RED}down{COLOR_RESET}') + + +class ClearStoresCommand(BaseCommand): + name = 'clear-stores' + desc = 'clear the contents of deployment\'s sqlite3 stores' + + def run(self, args): + with command_connect(self, 'combined') as con: + res = con.sudo('/opt/veraison/bin/veraison clear-stores', + user='veraison', echo=False, hide=True, pty=True) + if res.exited != 0: + self.fail(f'could not clear stores; got {res.exited}: {res.stdout}') + + +class CreateCertsCommand(BaseCommand): + + name = 'create-certs' + desc = 'generate certificates for the deployment' + + all_services = ['vts', 'provisioning', 'verification', 'management', 'keycloak'] + + def update_arguments(self, parser): + parser.add_argument('-c', '--ca-cert') + parser.add_argument('-k', '--ca-cert-key') + parser.add_argument('-s', '--service', + action='append', dest='services', choices=self.all_services) + + def run(self, args): + self.logger.info('creating service certificates...') + if (not args.ca_cert) != (not args.ca_cert_key): + self.fail('if one of -c/--ca-cert and -k/--ca-cert-key is specified, ' + 'the other must be as well') + + combined_host = self.cache.get('instances', {}).get('combined', {}).get('dns_name') + if not combined_host: + self.fail('did not find DNS name for combined instance in cache; ' + 'has create-stack been called?') + + keycloak_host = self.cache.get('instances', {}).get('keycloak', {}).get('dns_name') + if not keycloak_host: + self.fail('did not find DNS name for keycloak instance in cache; ' + 'has create-stack been called?') + + if not os.path.isdir(self.cache.certs_dir): + self.logger.debug(f'creating {self.cache.certs_dir}') + os.makedirs(self.cache.certs_dir) + + if args.ca_cert: + self.logger.debug('copying CA cert to cache') + shutil.copyfile(args.ca_cert, self.cache.ca_cert_path) + shutil.copyfile(args.ca_cert_key, self.cache.ca_key_path) + os.chmod(self.cache.ca_key_path, stat.S_IRUSR | stat.S_IWUSR) + + with open(self.cache.ca_key_path, 'rb') as fh: + ca_key = serialization.load_pem_private_key(fh.read(), None) + with open(self.cache.ca_cert_path, 'rb') as fh: + ca_cert = x509.load_pem_x509_certificate(fh.read()) + else: + if os.path.isfile(self.cache.ca_cert_path) and not args.force: + self.logger.debug('using existing CA cert') + with open(self.cache.ca_key_path, 'rb') as fh: + ca_key = serialization.load_pem_private_key(fh.read(), None) + with open(self.cache.ca_cert_path, 'rb') as fh: + ca_cert = x509.load_pem_x509_certificate(fh.read()) + else: + self.logger.debug('creating CA cert') + ca_cert, ca_key = create_ca_cert(self.cache.ca_cert_path, self.cache.ca_key_path) + + cache_entries = self.cache.get('certs', {}) + services = args.services or self.all_services + for service in services: + self.logger.debug(f'creating cert for {service}') + service_host = keycloak_host if service == 'keycloak' else combined_host + + cert_path = os.path.join(self.cache.certs_dir, f'{service}.crt') + if os.path.isfile(cert_path) and not args.force: + self.fail(f'cert for {service} already exists; use --force to overwrite') + + cert_path, key_path = create_service_cert( + service, service_host, self.cache.certs_dir, ca_cert, ca_key, + ) + cache_entries[service] = {'cert': cert_path, 'key': key_path} + + self.cache['certs'] = cache_entries + self.logger.info('done.') + + +class SetupServicesCommand(BaseCommand): + + name = 'setup-services' + desc = 'set up the services instance, updating certs and configuration' + + def update_arguments(self, parser): + parser.add_argument( + '-c', '--services-config-template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/combined-services-config.yaml.template', + )), + ) + + def run(self, args): + self.logger.info('setting up services on the combined instance...') + + certs = self.cache.get('certs') + if not certs: + self.fail('certificates have not been created; run create-certificates command') + + kc_host = self.cache.get('instances', {}).get('keycloak', {}).get('dns_name') + if not kc_host: + self.fail('could not find Keycloak host; run create-combined-stack command') + os.environ['KEYCLOAK_HOST'] = kc_host + + rds_settings = self.cache.get('rds') + if not rds_settings: + self.fail('could not find RDS settings in cache; run setup-rds command') + for k, v in rds_settings.items(): + os.environ[k] = str(v) + + config_path = command_instantiate_template( + self, args.deployment_name, args.services_config_template) + + self.logger.debug('connecting to combined instance...') + with command_connect(self, 'combined') as con: + self.logger.debug('tranfering updated config...') + con_transfer_file(con, + config_path, f'/opt/veraison/config/services/config.yaml', + owner='veraison:veraison') + + self.logger.debug('tranfering CA cert...') + con_transfer_file(con, + self.cache.ca_cert_path, '/opt/veraison/certs/rootCA.crt', + owner='veraison:veraison') + # delete any previous root key as it will no longer match the + # provision rootCA.crt (and is not needed on the services node). + con.sudo('rm -f /opt/veraison/certs/rootCA.key') + + for cname, cpaths in certs.items(): + if cname == 'keycloak': + continue + self.logger.debug(f'transfering {cname} cert and key...') + con_transfer_file(con, + cpaths['cert'], f'/opt/veraison/certs/{cname}.crt', + owner='veraison:veraison') + con_transfer_file(con, + cpaths['key'], f'/opt/veraison/certs/{cname}.key', + owner='veraison:veraison') + + self.logger.debug('restating veraison services...') + con.sudo('/opt/veraison/bin/veraison -s stop-services', + echo=False, hide=True, pty=True) + con.sudo('/opt/veraison/bin/veraison -s start-services', + echo=False, hide=True, pty=True) + + self.logger.info('done.') + + +class SetupKeycloakCommand(BaseCommand): + + name = 'setup-keycloak' + desc = 'set up the keycloak instance, updating certs and starting the service' + + def update_arguments(self, parser): + parser.add_argument('-r', '--realm-file') + + def run(self, args): + self.logger.info('setting up keycloak instance...') + + cpaths = self.cache.get('certs', {}).get('keycloak') + if not cpaths: + self.fail('certificates have not been created; run create-certificates command') + + self.logger.debug('connecting to keycloak instance...') + with command_connect(self, 'keycloak') as con: + self.logger.debug('transfering keycloak cert and key...') + con_transfer_file(con, + cpaths['cert'], '/opt/keycloak/certs/keycloak.crt', + owner='keycloak:keycloak') + con_transfer_file(con, + cpaths['key'], '/opt/keycloak/certs/keycloak.key', + owner='keycloak:keycloak') + + if args.realm_file: + self.logger.debug(f'transfering keycloak {args.realm_file}...') + filename = os.path.basename(args.realm_file) + con_transfer_file(con, + args.realm_file, f'/opt/keycloak/data/import/{filename}', + owner='keycloak:keycloak') + + self.logger.debug('stopping keycloak...') + con.sudo('systemctl stop keycloak') + + self.logger.debug('rebuilding keycloak...') + con.sudo('/opt/keycloak/bin/kc.sh build', hide=True) + + self.logger.debug('starting keycloak...') + con.sudo('systemctl start keycloak') + + self.logger.info('done.') + + +if __name__ == '__main__': + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + handler.setFormatter(LogFormatter()) + logging.basicConfig(level=logging.INFO, handlers=[handler]) + logging.getLogger('botocore').setLevel(logging.WARNING) + logging.getLogger("paramiko").setLevel(logging.WARNING) + + aws = Aws( + aws_access_key_id=os.getenv('AWS_ACCESS_KEY'), + aws_secret_access_key=os.getenv('AWS_SECRET_KEY'), + aws_session_token=os.getenv('AWS_SESSION_TOKEN'), + profile_name=os.getenv('AWS_PROFILE'), + ) + + cmd_map = {} + for name, cmd_cls in inspect.getmembers( + sys.modules[__name__], + lambda x: inspect.isclass(x) and issubclass(x, BaseCommand) and x is not BaseCommand): + if not name[0].isupper(): + continue # ignore variable bindings + assert cmd_cls.name, f'{cmd_cls} does not define a name' + cmd = cmd_cls(aws) + assert cmd.name not in cmd_map, f'duplicate name {cmd.name}' + cmd_map[cmd.name] = cmd + for alias in cmd.aliases: + assert alias not in cmd_map, f'duplicate alias {alias}' + cmd_map[alias] = cmd + + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--deployment-name', + help='the name for this deployment; this is used in a number ' + 'places, including AWS resources tags') + parser.add_argument('-f', '--force', action='store_true', + help='force overwrite of exiting resources') + parser.add_argument('-W', '--wait-period', type=int, default=1, + help='period (in seconds) to wait between polls to AWS for ' + 'long-running command progress') + parser.add_argument('-v', '--verbose', action='store_true', + help='show DEBUG level messages') + parser.add_argument('-q', '--quiet', action='store_true', + help='hide INFO level messages') + parser.add_argument( + '--cache-dir', default=xdg.BaseDirectory.save_data_path('veraison/aws'), + help='location that will be used for local deployment data', + ) + + subparsers = parser.add_subparsers(dest='command', required=True) + for name, command in cmd_map.items(): + if name == command.name: + command.register(subparsers) + + args = parser.parse_args() + + os.makedirs(args.cache_dir, exist_ok=True) + current_deployment_path = os.path.join(args.cache_dir, 'current_deployment') + if args.deployment_name: + with open(current_deployment_path, 'w') as wfh: + wfh.write(args.deployment_name) + else: + if os.path.isfile(current_deployment_path): + with open(current_deployment_path) as fh: + args.deployment_name = fh.read().strip() + else: + logging.critical('no current deployment exists; ' + 'please use -d/--deployment-name to specify') + sys.exit(1) + + cmd = cmd_map[args.command] + try: + cmd.execute(args) + except Exception as e: + cmd.logger.critical(f'{e.__class__.__name__}: {e}') diff --git a/deployments/aws/deployment.cfg b/deployments/aws/deployment.cfg new file mode 100644 index 00000000..155b0a90 --- /dev/null +++ b/deployments/aws/deployment.cfg @@ -0,0 +1,59 @@ +############################################################################## +# Veraison Deployment Configuration +# +# Note: this uses Bash syntax, however there is no need to export variables +# here, as this file will be sourced with set -a +# +# Note: in addition to settings here, you need to configure access to your +# AWS account. Please see: +# +# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html +# +############################################################################## +# shellcheck disable=SC2034 + +# The ports on which services will be listening. +VTS_PORT=${VTS_PORT:-50051} +PROVISIONING_PORT=${PROVISIONING_PORT:-8888} +VERIFICATION_PORT=${VERIFICATION_PORT:-8080} +MANAGEMENT_PORT=${MANAGEMENT_PORT:-8088} +KEYCLOAK_PORT=${KEYCLOAK_PORT:-11111} + +# The location of the Python venv that will be used to run the deployment +# script. This venv must have appropriate dependencies installed (see +# misc/requirements.txt). The environment may be initialized using the +# bootstrap command of the deployment.sh script. +VERAISON_AWS_VENV=${VERAISON_AWS_VENV:-~/venv/aws} + +# The name of the deployment. This will be used to name the CloudFormation +# stack. Additionally, all crated resources will have a tag with key +# "veraison-deployment" and this value. +VERAISON_AWS_DEPLOYMENT=${VERAISON_AWS_DEPLOYMENT:-veraison-deployment} + +# The name of the AWS region into which Veraison will be deployed. Must be a valid +# AWS region name, see: +# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions +VERAISON_AWS_REGION=${VERAISON_AWS_REGION:-eu-west-1} + +# ID of the VPC into which the deployment will be created. This must exist in +# the account. +VERAISON_AWS_VPC_ID=${VERAISON_AWS_VPC_ID:-} + +# ID of the subnet inside which the deployment instances will run. This must +# exist in the VPC. This is REQUIRED to be set by the user. +VERAISON_AWS_SUBNET_ID=${VERAISON_AWS_SUBNET_ID:-subnet-097626f1ba4f135ed} + +# Instances' security groups will be configures to allow connections from this +# CIDR. +VERAISON_AWS_ADMIN_CIDR=${VERAISON_AWS_ADMIN_CIDR:-217.140.96.0/20} + +# A comma-separated list of subnet IDs that will be used for the RDS subnet +# groups. This must contain at least two IDs inside different availability +# zones. This is REQUIRED to be set by the user. +VERAISON_AWS_RDS_SUBNET_IDS=${VERAISON_AWS_RDS_SUBNET_IDS:-subnet-097626f1ba4f135ed,subnet-019831fbe133be4a3} + +# Keycloak initial admin account credentials. If the password is not specified, it +# will be randomly generated during image creation, and will be written into the +# deployment cache. +KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN:-admin} +KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD:-} diff --git a/deployments/aws/deployment.sh b/deployments/aws/deployment.sh new file mode 100755 index 00000000..9475c146 --- /dev/null +++ b/deployments/aws/deployment.sh @@ -0,0 +1,197 @@ +#!/bin/bash +set -ueo pipefail + +_error='\e[0;31mERROR\e[0m' +_this_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +_repo_root=$(realpath "${_this_dir}/../..") + +set -a +source "${_this_dir}/deployment.cfg" +set +a + +_script=${_this_dir}/bin/veraison + +function help() { + set +e + local usage + read -r -d '' usage <<-EOF + Usage: deployment.sh [OPTIONS...] COMMAND [ARGS...] + + This script allows deploying Veraison to AWS + XXX TODO XXX + + OPTIONS: + + Please note tht opitons MUST be specified before the command and arguments. + + -h show this message and exist + + COMMANDS: + + help + Show this message and exit. The same as -h option. + + bootstrap + Initialize the Python venv that will be used to run the deployment script. + + EOF + set -e + + echo "$usage" +} + +function bootstrap() { + "${_repo_root}/deployments/debian/deployment.sh" bootstrap + + case $( uname -s ) in + Linux) + # shellcheck disable=SC2002 + local distrib_id + distrib_id=$(head -n 1 /dev/null | \ + cut -f2 -d= | tr -d \") + + case $distrib_id in + Arch) sudo pacman -Syy packer ssh openssl;; + Ubuntu) + sudo apt update + sudo apt --yes install curl openssl + + curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - + sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" + sudo apt --yes install packer + ;; + *) + echo -e "$_error: Boostrapping is currently only supported for Arch and Ubuntu." + exit + ;; + esac + ;; + Darwin) + if ! type brew > /dev/null; then + echo -e "$_error: homebrew (https://brew.sh) must be installed." + exit 1 + fi + brew install packer + ;; + *) + echo -e "$_error: Boostrapping is currently only supported for Arch, Ubuntu, and MacOSX (via homebrew)." + exit + ;; + esac + + python -m venv "${VERAISON_AWS_VENV}" + # shellcheck disable=SC1091 + source "${VERAISON_AWS_VENV}/bin/activate" + pip install -r "${_this_dir}/misc/requirements.txt" + + set +e + local message + read -r -d '' message <<-EOF + + Enviroment for AWS deployment has been bootstraped. To activate it: + + source ${_this_dir}/env/env.bash + + EOF + set -e + + echo "$message" +} + +function bringup() { + _check_installed openssl + _check_installed packer + + veraison configure --init \ + --vpc-id "${VERAISON_AWS_VPC_ID}" \ + --subnet-id "${VERAISON_AWS_SUBNET_ID}" \ + --rds-subnet-ids "${VERAISON_AWS_RDS_SUBNET_IDS}" \ + --admin-cidr "${VERAISON_AWS_ADMIN_CIDR}" \ + --region "${VERAISON_AWS_REGION}" + + veraison create-deb + veraison create-key-pair + veraison create-combined-image + veraison create-keycloak-image + veraison create-combined-stack + + veraison update-security-groups + veraison create-certs --ca-cert "${VERAISON_CA_CERT}" \ + --ca-cert-key "${VERAISON_CA_CERT_KEY}" + veraison setup-rds + veraison setup-keycloak --realm-file "${_this_dir}/misc/veraison-realm.json" + veraison setup-services +} + +function redeploy_stack() { + _check_installed openssl + + veraison delete-stack combined + veraison delete-certs + + veraison create-combined-stack + veraison update-security-groups + veraison create-certs --ca-cert "${VERAISON_CA_CERT}" \ + --ca-cert-key "${VERAISON_CA_CERT_KEY}" + veraison setup-keycloak --realm-file "${_this_dir}/misc/veraison-realm.json" + veraison setup-services +} + +function teardown() { + veraison delete-stack combined + veraison delete-certs + veraison delete-image keycloak + veraison delete-image combined + veraison delete-key-pair + veraison delete-deb +} + +function veraison() { + "${_script}" "${_script_flags[@]}" "${@}" +} + +function _check_installed() { + local what=$1 + + if [[ "$(type -p "$what")" == "" ]]; then + echo -e "$_error: $what executable must be installed to use this command." + exit 1 + fi +} + +_force=false +_verbose=false + +while getopts "hfv" opt; do + case "$opt" in + h) help; exit 0;; + f) _force=true;; + v) _verbose=true;; + *) break;; + esac +done + +shift $((OPTIND-1)) +[ "${1:-}" = "--" ] && shift + +_script_flags=(--deployment-name "${VERAISON_AWS_DEPLOYMENT}") +if [[ $_force == true ]]; then + _script_flags+=(--force) +fi +if [[ $_verbose == true ]]; then + _script_flags+=(--verbose) +fi + +_check_installed python + +_command=$1; shift +_command=$(echo "$_command" | tr -- _ -) +case $_command in + help) help;; + bootstrap) bootstrap;; + bringup) bringup;; + redeploy-stack) redeploy_stack;; + teardown) teardown;; + *) echo -e "$_error: unexpected command: \"$_command\"";; +esac +# vim: set noet sts=8 sw=8: diff --git a/deployments/aws/env/env.bash b/deployments/aws/env/env.bash new file mode 100644 index 00000000..e562901e --- /dev/null +++ b/deployments/aws/env/env.bash @@ -0,0 +1,13 @@ +_this_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +_deployment_root="${_this_dir}/.." +_deployment_cfg="${_deployment_root}/deployment.cfg" + +set -a +# shellcheck source=../deployment.cfg +source "${_deployment_cfg}" +set +a + +# shellcheck disable=SC1091 +source "${VERAISON_AWS_VENV}/bin/activate" + +export PATH="${_deployment_root}/bin":${PATH} diff --git a/deployments/aws/misc/requirements.txt b/deployments/aws/misc/requirements.txt new file mode 100644 index 00000000..0819375b --- /dev/null +++ b/deployments/aws/misc/requirements.txt @@ -0,0 +1,9 @@ +boto3==1.35.8 +botocore==1.35.8 +cryptography==43.0.1 +envsubst==0.1.5 +fabric==3.2.2 +pyxdg==0.28 +PyYAML==6.0.2 +requests==2.32.3 +sqlitedict==2.1.0 diff --git a/deployments/aws/misc/veraison-realm.json b/deployments/aws/misc/veraison-realm.json new file mode 100644 index 00000000..0fd76cb4 --- /dev/null +++ b/deployments/aws/misc/veraison-realm.json @@ -0,0 +1,1841 @@ +{ + "id" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "realm" : "veraison", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 1800, + "accessTokenLifespanForImplicitFlow" : 1800, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "3c14c82e-ef6e-4c35-8fc5-b95322e12698", + "name" : "manager", + "description" : "Manages policies.", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "29a6925f-81e8-4177-bb95-3c36bf4b7645", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "03e833d2-8f65-4e1d-848a-87a01650b4d5", + "name" : "provisioner", + "description" : "Provisions trust anchors and endorsements.", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "3c85b41b-2cd1-40af-9e31-c6a9d24114ce", + "name" : "default-roles-veraison", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "a40e1662-6ddb-4fd7-829a-ffece56b48d2", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "66fc745f-b46a-4efa-bd17-77e060d7b2a0", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "b0d0efb7-3691-4ead-b33a-f16191bc5789", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-realm", "view-authorization", "view-clients", "manage-identity-providers", "query-clients", "query-groups", "manage-events", "create-client", "manage-users", "manage-clients", "view-users", "view-identity-providers", "manage-realm", "manage-authorization", "query-realms", "impersonation", "query-users", "view-events" ] + } + }, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "a34cb720-d7c5-408b-8b52-ed426d6d809c", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "44017973-fe85-45b5-b7f5-f53a757bce73", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "00eb870e-9ca7-4933-8809-4c536c1a22f6", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "c1fa45d6-dd02-4487-9cc2-6293f825467b", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "0d5c3ba5-7763-472c-9239-f946931e413f", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "fcc850bb-f082-4dd6-b5ab-510d1cc89311", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "2b8e4b72-1c63-4528-9be5-271868c1372a", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "bcf9a96e-d875-4548-adb0-8768404e54f7", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "a9cb8f89-fec3-4679-a0fc-3557cacc8c0f", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "9186b791-8864-4373-85d4-f27a4895ec9b", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "67eeb9b5-6368-4489-9606-d04a7c78e330", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "50da5487-88c2-4c51-b95c-a7249e283df2", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "aca9a2d7-f1bc-4b2e-827d-d366fdfce45a", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "93cdc235-c527-4818-9601-3f27503a5988", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "41419687-e0a3-4a1b-a380-8a200f2ae2a8", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "66c8d39f-46de-4861-934e-440998d5e427", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "50d9bd9d-d100-48be-8239-2661a6a13ca7", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "cdeea646-d366-479d-b2d2-40853d6d363d", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "e9f5fb23-6688-4a97-897c-fe9f6a370c64", + "attributes" : { } + } ], + "account" : [ { + "id" : "0b90f930-8203-4599-be96-e5a5cac8ee7b", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "2c7d0710-a3d8-4a40-9f2e-8143f2e9e153", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "d3434c63-75fc-4ac2-af54-91ce26ace472", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "7846f314-a5f3-4bac-a02c-7da437c300ce", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "2f69f35f-6748-4002-aecf-85d194c28d94", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "1edba5fd-9d7e-4a15-8dcf-3a14a4b738cc", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "ce9b2c54-d838-42ae-bbe6-d6ee70e46902", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "4df2f06c-a286-4f29-bb5e-c5259780099c", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + } ], + "veraison-client" : [ ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "3c85b41b-2cd1-40af-9e31-c6a9d24114ce", + "name" : "default-roles-veraison", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "778a3424-b538-415c-b9c6-ade0e483d818", + "createdTimestamp" : 1692274136073, + "username" : "veraison-manager", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "086eb891-30fd-408d-91a0-7d4c8a701f54", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1692274151671, + "secretData" : "{\"value\":\"KDJZv/qvMYMCb5v18ymqGrKy9ZPk/3zB3WvrzwFogRI=\",\"salt\":\"zU0ayGQFxfgyU2T2EU90+w==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "manager", "default-roles-veraison" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3d0bfa21-50b0-496a-9a22-c6e1f63238f9", + "createdTimestamp" : 1692274044033, + "username" : "veraison-provisioner", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "0dd61192-932a-4972-a168-2c4867673396", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1692275392983, + "secretData" : "{\"value\":\"+uFdoOr+hk62Z87HGA9RvWcXhJMNX4YHPjmkjJSK16U=\",\"salt\":\"s/dmj1YbJ+/yLdbnmAg/8Q==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "provisioner", "default-roles-veraison" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/veraison/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/veraison/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "0124f2b3-be1b-49b5-a113-352d1ffe299a", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/veraison/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/veraison/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "3b63fc08-5779-4dc5-a2ce-5a22a201fd49", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "b2e061bf-883b-4534-925b-f3aafb433fd7", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "e9f5fb23-6688-4a97-897c-fe9f6a370c64", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "4882f437-a423-46a1-878c-10616b7d6117", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "9fc0fda8-3c5e-46d4-ae64-126fa11ecbb3", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/veraison/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/veraison/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "7bd436f6-4a54-425e-bb6c-7de806fcf84d", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "30ea0b69-30fe-46bd-82f7-ac1f980b6928", + "clientId" : "veraison-client", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "YifmabB4cVSPPtFLAmHfq7wKaEHQn10Z", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1692267068", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "display.on.consent.screen" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "cdb2454d-355f-46e6-8c75-5ee7d104011d", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "5a0ee988-c3b5-400d-a77a-b6db95b44583", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "57f0aa40-359f-4f54-8413-1c414d95dc62", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "2fb49e8e-fceb-4ebf-a4f8-b7b074d076fb", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "fe6d8736-c643-4189-bff0-22179ccd0173", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "1407f674-94da-4baf-85a7-4825673df97a", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "7d79f487-aa57-414e-b68f-b731056aef2a", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "d2296431-072a-4bf1-87be-6e0f390e255a", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "162ede80-8a96-47ae-9671-0c6eb1bfe520", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "c93b9d49-5826-45ac-b05a-c4c5fafa5b96", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "e7cf775c-3af5-4ae4-aaf4-85a84999d064", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "15c2e269-faff-4013-a68d-ed20ea46031e", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "70ad35eb-7736-487d-b4c0-42fd4fe0d563", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "b4b48ad4-f324-47d9-a0dc-7d5a47c5126d", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "ed92a954-3a49-4cda-ad97-c5c605639344", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "622a036e-ec1b-4591-8304-38f6383d5405", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "a53cf6f7-1ecc-403d-9574-f0691388e401", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "798ddcba-546b-452d-9d15-27cd5b12b1be", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "801544eb-e426-478f-b9f1-d9292d521147", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "7461381f-3dbf-49d7-b033-619b0148ca09", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "01d158bf-8c4f-4b3d-b84a-141e114c0b8f", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "4eb1eb4a-5f27-434b-a141-a9f6902d93d5", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "3f241149-6525-4a09-89da-649094ab0e47", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "43909b68-6505-4119-abd9-df8447ab6dfc", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "1caf557c-c8c4-4e5e-bf42-20365baa40e1", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "c8fa1b3b-d9ef-4fef-b291-35318d6817ad", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "e1fbb06d-9b7f-4e2f-ae8c-a2d0cd41dc76", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "b3875b32-f72d-4fed-ac28-fe48b94261bf", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "e9094f82-a0ed-47e2-b484-377e55e24d68", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "23735c41-4b70-46b0-8bd8-66d6db6ae200", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "4d7eb99e-09bb-450f-9f22-324d52dc58da", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "70c1a038-13c6-405a-8e90-ea23930a7ff3", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "e24a3896-7bc6-4007-9582-852cf918aaab", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "523c6a94-87a6-4022-bdbf-842b2b2a2ac4", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "4adfae7f-d902-467f-91a0-22bbfa3b845f", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "a867d920-baaf-424b-8e56-8c941f79bbd9", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "da651414-ffb1-4e47-a2ca-cdd1c6270481", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "838db39f-26e9-49f4-a032-522e8a817212", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "5516ee07-cd4c-40c2-8ee0-c39010f6c521", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper" ] + } + }, { + "id" : "961ce4e7-198c-49bc-b988-6b87f313b89b", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "ad3a8ca5-9bd9-4c97-a7c7-ec04a2ad6279", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "875ae783-21e0-4971-8643-e0d9dfbce3e1", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper" ] + } + }, { + "id" : "4bce5375-223e-41cd-af02-1d3813895b07", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "23ace297-c838-4fa3-ae94-3f0e01d257d5", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "69da926b-3bb5-433c-80bd-906a4180a8bf", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "94bc520a-7994-42a9-8083-3c185e66eca8", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "a7e63c45-88cb-4a77-a458-a55473909d17" ], + "secret" : [ "tbWy-FjQ4PPZClLRCByN5PAWOh5oHwlPjJklDF-WyYpr4WCQnfBnUPnBdEI6ThyGEZ1oSGEq3wnivoQMARYLOA" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "ea51c427-9158-4564-ad9c-fccff828ea2a", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpQIBAAKCAQEAwQ1evDFc9jC1e6SZuM4oAVs+O1Mj8iXK6otj4aYG5ox2I9Vg2AjaF53oX0b9PIUyvkdxpPYBPIqkfe7P6woXjOZEiUo/vQfR79qdjwjfWLhOGuLezj2OQjAEzRdg4A2LhMDYv8rc5S3aTJ1Q7hdXe+ETDg6o/2ou/IJyIXcz9HDnIelrtPTemmQbrSFvYh8NLJwLqLfa4KxCVnUiwOGWed6L96lV8Z6W+R7ssN/XekaPf8fsfxXn4DFU7mGHEpjEfInUHg7oQMSFotCbDFg1JFKLRIFOZvo3690cgdf/VaeWvYuc5WEK+hEHq2POXIYTpUtNndGasdKQBnhY88ZZqwIDAQABAoIBAAWMOCVkWG2NZ3RkKIlnsz1GvU/AdeSm3WFGxtoHMWToCG/DLq3xTdh75d+cZKPxQqIL34zZXPO7xY+w5dy1RN6nB1/X+GOa7peGoBx/fsZH6vFZhQRG6ehRBPJNMlrTSNJ0evDCsv0LB55IBGQIhlpoVL8vVf/xBzn5GFqJ2GkKgMdUTVeLIOt48V/raoN4lS0g8jJMypOorXs17JLPKsECOFm4VKKzYQ/S5JHi/Cq9PcSex+V2nD/blh5FqbVWYxGP5hFPHx0N37Hi8fxZAXAT8mHShNyT7NnPsdgy6kEJ09tET9KP3A7ymlzXD8BXoWEKBMvvepfyle4GTNxDuuECgYEA46+2yQaAINnKMzFcI4Smk8H8vLM3lXl5bXWBqYTx4FeB0VOsLBn46IOu8pxJw2nx2htPmKUDkcQZO0NqOz3MTIxVgjwidRnzmp3p0K4GDeKdnO7KGyDxScQxqNl4zuQc/l15YfQN09jBxt6WtjF+qXUV6qGiNHGkQFxMRF4T6LMCgYEA2Q8YLIjgJybWsHQ7fvHNNMQjQGGRAdSWRHJLfpJU/icfjrLCB/3i+Pc17UmovRtJIYUzHVs54rAB5tl7yNc7g0XMhTCxCV/0ETPYlKL3Yb18aZvSvwp7R3Nxru/y1l/E1R9vyF8bwzVthe2Any53Ru/cJhhUknXZPHYI4++aFykCgYEA1kI8V9/uIvvP82y3sBTcTJ94HnroC5lMU10Ir2WT1/GBEGMU2kt2mBeTQmsgXuwL05tvw81FFp7av5IpHaaB4mcM8Il2Q4wwWYfQx7d7qwVeHJf5SJ4vcaNWt/YuYUL4pcWAvFTVzk1jzKkaWkkpUH4GKc4AvilSz94LRyrgwVECgYEArAnytxml0GJQN3tozK0KYJA1AIpUTIcasxWEEMYa53ZK5Od6MqtggsQt0e1X+Mrvo8nXQaVUs/+dAkPOgNlXKizgdZCqQSv0Xs4hE243dRiiy3HeD91W6MLvkCBO8OrnL6TDDKWVc5udO1GLaJ+Dmo3yh58xKQSPMgS79y2pjEECgYEA2yjd8vfqRVcsnz6LimX2YIMIlRYd867de4sEIhJHSfXspxD5UgVj9cxn6BUvzAafFH1cFV+LR7DU4IjgBsWsYWYq1Kt1SXrrxwh7LmYT6PX2z7jfk6jRcpsZPNJdCDqmW03Ex+xLGUKKj7C5vMhe/fZqV383rGTKPuO81AVxjKI=" ], + "certificate" : [ "MIICnzCCAYcCBgGKA2Q2rTANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAh2ZXJhaXNvbjAeFw0yMzA4MTcxMjA1MTNaFw0zMzA4MTcxMjA2NTNaMBMxETAPBgNVBAMMCHZlcmFpc29uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQ1evDFc9jC1e6SZuM4oAVs+O1Mj8iXK6otj4aYG5ox2I9Vg2AjaF53oX0b9PIUyvkdxpPYBPIqkfe7P6woXjOZEiUo/vQfR79qdjwjfWLhOGuLezj2OQjAEzRdg4A2LhMDYv8rc5S3aTJ1Q7hdXe+ETDg6o/2ou/IJyIXcz9HDnIelrtPTemmQbrSFvYh8NLJwLqLfa4KxCVnUiwOGWed6L96lV8Z6W+R7ssN/XekaPf8fsfxXn4DFU7mGHEpjEfInUHg7oQMSFotCbDFg1JFKLRIFOZvo3690cgdf/VaeWvYuc5WEK+hEHq2POXIYTpUtNndGasdKQBnhY88ZZqwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBykUMKuwsNci+zvJdnRPINREzBxVJGbpwJTYdWT8dNYHLakP0HhRXDqAzWdBQkuqSTroUjCSxKsLfnoon3dAQkYM/64yCHFo13a1wtg0pJw0o8BTM5j2TrjnFWvj7XR0xiRGpj2yz0as4gN29Gj3Atw1IEkBnSO4BySEyTou7fGEqWcE6OUWAeyA5OCuCs7geSbghNNCrCMnyKAGLIgidGI7CgopZMQqJ/UoiPXHNuor+N3TMiDYZ3knsi9jY8bJ1DixbkjOVmaG3VI7z5iwNF7jrOpY7XR9SMKt32QFxf1VXMGzygiBoRPIpt6OENGO4naJTIAVLwaZxaUxClOz+G" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "af7c0bdc-1ea0-4c0a-9a95-035e49102c23", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEApJR7Ia2PCedukU9Rsb4CPlzs4UNiAIk5ou/4bTLEMQx8Cb6P4UNJoG95Wpf8jsaZtRUVei+SB7BWLfcPi4w7x6JRhrS67vOxH+P4BbtMqdg5aID/fCxUTMJMiSWx9R39PC6mFzc4ZvxzBvNmIbBBiFWuIHeYZOR/3lcrLTh1r9pCYywRzGPbY0u/8CWDxaQJGrNSGjnEXqL97TTuMuu2t07XJAglPsJR63fxOqgd5RET39uDHQqbLy7bxNiCq9+vMcF6JTr13KYDHmtb4qPT95zoqhRHrCI7EI+ruozpoQG6OV/IOXGJhYyFBjMZLCOIMWWoX510QPmQyRNwHsUT3wIDAQABAoIBABpmLXYSe+jlHumKD/BIf92hqZGHQyITi5OPld7A5OglONmV9UhMiHPb+FryxDKMDWhsMFzADVbUZI53nHO66R/gRomAAKLcTg4afX9AiE86KBiTkJJKHgo4pG7fWrNBveFjGNs3zVafqefJ2m5kR668Z5488MybkR3eB5v3ptEFGgU2ruCFbcLSLM0Rg5MwvXuxNrBFLLvQFsAatofFuDEcGHEXX6Rc+3WY8zK/rvFPhB1O2hCuoj+Ka2NcjEQgetlK4KKYBm42mwV0vqNE6t8ewR3+MvJnr6fWOfTlqR+UQ4BbPEteXrV8Hwi4hSAWrrr9MZgBBzUeKroSHgsAUrkCgYEAzhDK4FQCuEGCoEFVKGGD996qXI1j+yLXC4QrIqHyu8Al+GNpJwVgsUWsF9MfhVNjtctBb2qkIdwfV3nlAA2Sg8gHSDVWA5hAgK8d05qCj3aEuxk/xkb2naluuAvztycVaub7Wn54hq8pY3wOifgMl4Qz+7FoAxFNv8HAgrgIJxUCgYEAzHYk7VXjmiTY9DEXXnd6SugVZu5WTSqykPB7nFfosIIdgi0uInFHdhDMdHR97dW7GZkMB1p8zoVdSrJaU6iiDelfbXFX69tQtPbi3eVqls5htUuvozCNyz7eeZnHQ2EKsxpIZC+OOWHTJbUtGDYRG6yvOqqGR1ZTUNUB9g03zCMCgYB8ABTlKwi78gf2AXqKIywzo1Um/ppUjGGVd4Ixg/y6SGVQ9BlZtt25rzBg4dXM+CI/SkFlF2oPShO+IwbPolsxW9Qt+pJ49UyTY01ygT7hr7Mtl4MOALP0qfmLXP3aj/VOcBJ/IS3L9mnUiNmC4rZJEu/pHJd3iRkdNC1xO+cEBQKBgQCGenqFQ8Wkn/G2gwds0ca0t/tDrSVEMf4qyJF03nkkhyAje9XpP3qSFDB1tB0Trk0WZAx+VazbJOqcc7xnY/XakpF6aV87uQ9XRz8mVXuK3wly9en6ure4Y4xujI98KLqh3HqaspCn+0imd4jGcOFFw4mpW3lgOE4qTz+v9zeo4wKBgQCNdhsXJs1gGwgwr5cyUA6XWvoXt5XaQPbSDe8g1h5Va4K28aU3Z+FoDrq6xCkb7klk38S0grQmykGXnVKw/6R09I3WRwe7U9OGjxiN99jDG/wc0qIxdW2XxlvPgphi4AR/XbD16hvhVHecL3p4frjJAu2kp7GFieNcVFPjtcQm2g==" ], + "certificate" : [ "MIICnzCCAYcCBgGKA2Q3iTANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAh2ZXJhaXNvbjAeFw0yMzA4MTcxMjA1MTRaFw0zMzA4MTcxMjA2NTRaMBMxETAPBgNVBAMMCHZlcmFpc29uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJR7Ia2PCedukU9Rsb4CPlzs4UNiAIk5ou/4bTLEMQx8Cb6P4UNJoG95Wpf8jsaZtRUVei+SB7BWLfcPi4w7x6JRhrS67vOxH+P4BbtMqdg5aID/fCxUTMJMiSWx9R39PC6mFzc4ZvxzBvNmIbBBiFWuIHeYZOR/3lcrLTh1r9pCYywRzGPbY0u/8CWDxaQJGrNSGjnEXqL97TTuMuu2t07XJAglPsJR63fxOqgd5RET39uDHQqbLy7bxNiCq9+vMcF6JTr13KYDHmtb4qPT95zoqhRHrCI7EI+ruozpoQG6OV/IOXGJhYyFBjMZLCOIMWWoX510QPmQyRNwHsUT3wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA/zFEkpuhffnzxjKRZU05qjA0fEOp7t8C6YI0ERd+bhp0wS5thicSCPhTZgIe3RH23Ozd8lbsmBpnqsOg2PGw/mVjFcutTZrHObCYmmAIQBYlfwfC4UqIx5UjjY+3H/mdsS9XsUAtR9OIuK8NBTSZqHTIK56ifYuifvpVUVbotqM+Yg4r1D6lPBwsvo9OdRxKODrEocfwmMjUJfNX4p1ywXE8sazlBm0CbHASugLEBRU5fQAig+D6RyEijPBXKGrVJxXOwEGO/nukkgmPYruPwNy+h3WmURwkQ5IbnvTBacu9iTYMN/vaRRq1imHNWnjFbZnDCJr+TWIPbVQjY6FY4" ], + "priority" : [ "100" ] + } + }, { + "id" : "1351fa5d-2a4a-4780-8fae-d3927b012acb", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "b3f2725f-76e5-4f0d-9b86-328a2995ef85" ], + "secret" : [ "CC51rVYsGuwM7wnkjylqcA" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "2015fa69-e7e2-47f2-85a9-72083f2d5799", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "c33ebc38-f2c7-4f51-a53e-1ba9370f670a", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "edb6e773-66f4-4c5d-96e8-72161fd4aca9", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "22dda1be-7bc3-4bd9-9792-d54b114e7273", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cbf77531-658d-4b21-bce9-341e21357133", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "5df209de-e792-43dd-8fd8-ca9462d5c3d7", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "26fccf30-44b5-43a6-8bea-621d74ad48c4", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "a77263c0-036d-4b0f-a749-adf9bc01e32a", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e62b015b-62cf-44e9-aaef-b782278d1f94", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "d9f9ab6a-a6ae-4f8f-a411-8dd0a9a11a46", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b63e66d7-e30d-4458-b56c-e3d10c48ad9a", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "20dd3a17-943d-40a4-a320-b78a018c7740", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "8dc9f372-c43c-48c3-80a4-f7fccb183a10", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "2a615cce-f472-44dc-a379-c7b28ac78e2b", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e929e547-123a-4d1d-9d53-da597fda78ce", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "f2166939-6982-468f-9be9-0217f35c2386", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "6db0cf80-5f81-4bcf-8e0b-3c4a682995eb", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "8afa88d2-dbd0-4e51-bf01-3032c1221d34", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "629cf0f7-f153-4f85-9bfe-2f5c78491119", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "2ee0fe91-8964-4c02-982a-f3640f0fa9de", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "22.0.1", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} diff --git a/deployments/aws/templates/combined-services-config.yaml.template b/deployments/aws/templates/combined-services-config.yaml.template new file mode 100644 index 00000000..0f29e319 --- /dev/null +++ b/deployments/aws/templates/combined-services-config.yaml.template @@ -0,0 +1,60 @@ +logging: + level: info # valid levels: error, warning, info, debug + output-paths: + - stdout + - /opt/veraison/logs/{{ .service }}-stdout.log +provisioning: + listen-addr: 0.0.0.0:${PROVISIONING_PORT} + protocol: https + cert: /opt/veraison/certs/provisioning.crt + cert-key: /opt/veraison/certs/provisioning.key +verification: + listen-addr: 0.0.0.0:${VERIFICATION_PORT} + protocol: https + cert: /opt/veraison/certs/verification.crt + cert-key: /opt/veraison/certs/verification.key +management: + listen-addr: 0.0.0.0:${MANAGEMENT_PORT} + protocol: https + cert: /opt/veraison/certs/management.crt + cert-key: /opt/veraison/certs/management.key +vts: + server-addr: localhost:${VTS_PORT} + tls: true + cert: /opt/veraison/certs/vts.crt + cert-key: /opt/veraison/certs/vts.key + ca-certs: /opt/veraison/certs/rootCA.crt +ear-signer: + alg: ES256 + key: /opt/veraison/signing/skey.jwk +plugin: + backend: go-plugin + go-plugin: + dir: /opt/veraison/plugins/ +ta-store: + backend: sql + sql: + driver: pgx + datasource: postgres://${RDS_USER}:${RDS_PASSWORD}@${RDS_HOST}:${RDS_PORT}/${RDS_DBNAME} + tablename: trust_anchors +en-store: + backend: sql + sql: + driver: pgx + datasource: postgres://${RDS_USER}:${RDS_PASSWORD}@${RDS_HOST}:${RDS_PORT}/${RDS_DBNAME} + tablename: endorsements +po-store: + backend: sql + sql: + driver: pgx + datasource: postgres://${RDS_USER}:${RDS_PASSWORD}@${RDS_HOST}:${RDS_PORT}/${RDS_DBNAME} + tablename: policies +po-agent: + backend: opa +auth: + backend: keycloak + host: ${KEYCLOAK_HOST} + port: ${KEYCLOAK_PORT} + ca-cert: /opt/veraison/certs/rootCA.crt + +# vim: set ft=yaml: diff --git a/deployments/aws/templates/image-combined.pkr.hcl b/deployments/aws/templates/image-combined.pkr.hcl new file mode 100644 index 00000000..c158be03 --- /dev/null +++ b/deployments/aws/templates/image-combined.pkr.hcl @@ -0,0 +1,93 @@ +packer { + required_plugins { + amazon = { + version = ">= 1.2.8" + source = "github.com/hashicorp/amazon" + } + } +} + +variable "deployment_name" { + type = string +} + +variable "ami_name" { + type = string +} + +variable "vpc_id" { + type = string +} + +variable "region" { + type = string + default = "eu-west-1" +} + +variable "instance_type" { + type = string + default = "t2.micro" +} + +variable "subnet_id" { + type = string +} + +variable "deb" { + type = string +} + +locals { + dest_deb = "/tmp/${basename(var.deb)}" +} + +source "amazon-ebs" "ubuntu" { + ami_name = "${var.ami_name}" + instance_type = "${var.instance_type}" + region = "${var.region}" + vpc_id = "${var.vpc_id}" + subnet_id = "${var.subnet_id}" + associate_public_ip_address = true + tags = { + veraison-deployment = "${var.deployment_name}" + } + source_ami_filter { + filters = { + name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" + root-device-type = "ebs" + virtualization-type = "hvm" + architecture = "x86_64" + } + owners = ["099720109477"] # amazon + most_recent = true + } + security_group_filter { + filters = { + "tag:Class": "packer" + } + } + ssh_username = "ubuntu" +} + +build { + name = "veraison-combined" + sources = [ + "source.amazon-ebs.ubuntu" + ] + + provisioner "file" { + source = "${var.deb}" + destination = "${local.dest_deb}" + } + + provisioner "shell" { + inline = [ + "sudo dpkg -i ${local.dest_deb} 2>&1", + "sudo apt-get update", + "sudo apt-get install --yes sqlite3 jq 2>&1", + "echo \"\nsource /opt/veraison/env/env.bash\" >> ~/.bashrc " + ] + } +} + +# vim: set et sts=2 sw=2: diff --git a/deployments/aws/templates/image-keycloak.pkr.hcl b/deployments/aws/templates/image-keycloak.pkr.hcl new file mode 100644 index 00000000..7cb69a93 --- /dev/null +++ b/deployments/aws/templates/image-keycloak.pkr.hcl @@ -0,0 +1,121 @@ +packer { + required_plugins { + amazon = { + version = ">= 1.2.8" + source = "github.com/hashicorp/amazon" + } + } +} + +variable "deployment_name" { + type = string +} + +variable "ami_name" { + type = string +} + +variable "vpc_id" { + type = string +} + +variable "region" { + type = string + default = "eu-west-1" +} + +variable "instance_type" { + type = string + default = "t2.micro" +} + +variable "subnet_id" { + type = string +} + +variable "keycloak_version" { + type = string + default = "25.0.5" +} + +variable "conf_path" { + type = string +} + +variable "service_path" { + type = string +} + +locals { + conf_dest = "/opt/keycloak/conf/keycloak.conf" +} + +source "amazon-ebs" "ubuntu" { + ami_name = "${var.ami_name}" + instance_type = "${var.instance_type}" + region = "${var.region}" + vpc_id = "${var.vpc_id}" + subnet_id = "${var.subnet_id}" + associate_public_ip_address = true + tags = { + veraison-deployment = "${var.deployment_name}" + } + source_ami_filter { + filters = { + name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" + root-device-type = "ebs" + virtualization-type = "hvm" + architecture = "x86_64" + } + owners = ["099720109477"] # amazon + most_recent = true + } + security_group_filter { + filters = { + "tag:Class": "packer" + } + } + ssh_username = "ubuntu" +} + +build { + name = "veraison-keycloak" + sources = [ + "source.amazon-ebs.ubuntu" + ] + + provisioner "file" { + source = "${var.conf_path}" + destination = "keycloak.conf" + } + + provisioner "file" { + source = "${var.service_path}" + destination = "keycloak.service" + } + + provisioner "shell" { + inline = [ + "sudo apt-get update", + "sudo apt-get update", # doing it twice as once doesn't seem to be enough .... + "sudo apt-get install -f --yes openjdk-21-jdk 2>&1", + + "sudo groupadd --system keycloak", + "sudo useradd --system --gid keycloak --no-create-home --shell /bin/false keycloak", + + "wget https://github.com/keycloak/keycloak/releases/download/${var.keycloak_version}/keycloak-${var.keycloak_version}.tar.gz", + "tar xf keycloak-${var.keycloak_version}.tar.gz", + "rm keycloak-${var.keycloak_version}.tar.gz", + "sudo mv keycloak-${var.keycloak_version} /opt/keycloak", + "sudo mv keycloak.conf /opt/keycloak/conf/keycloak.conf", + "sudo mv keycloak.service /opt/keycloak", + "sudo mkdir -p /opt/keycloak/data/import", + "sudo mkdir -p /opt/keycloak/certs", + + "sudo chown -R keycloak:keycloak /opt/keycloak", + "sudo systemctl enable /opt/keycloak/keycloak.service", + ] + } +} + +# vim: set et sts=2 sw=2: diff --git a/deployments/aws/templates/keycloak.conf.template b/deployments/aws/templates/keycloak.conf.template new file mode 100644 index 00000000..bcadc72c --- /dev/null +++ b/deployments/aws/templates/keycloak.conf.template @@ -0,0 +1,7 @@ +# See https://www.keycloak.org/server/all-config for all alvailable configuration. +http-enabled=false +hostname-strict=false +https-port=${KEYCLOAK_PORT} +https-certificate-file=/opt/keycloak/certs/keycloak.crt +https-certificate-key-file=/opt/keycloak/certs/keycloak.key + diff --git a/deployments/aws/templates/keycloak.service.template b/deployments/aws/templates/keycloak.service.template new file mode 100644 index 00000000..e6d62ae1 --- /dev/null +++ b/deployments/aws/templates/keycloak.service.template @@ -0,0 +1,16 @@ +[Unit] +Description=Keycloak Authentication server +After=network.target + +[Service] +Type=exec +Environment="KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN}" "KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}" +WorkingDirectory=/opt/keycloak +ExecStart=/opt/keycloak/bin/kc.sh start --optimized --import-realm --verbose +Restart=always +User=keycloak + +[Install] +WantedBy=default.target + +# vim: set ft=systemd: diff --git a/deployments/aws/templates/stack-combined.yaml b/deployments/aws/templates/stack-combined.yaml new file mode 100644 index 00000000..172e5263 --- /dev/null +++ b/deployments/aws/templates/stack-combined.yaml @@ -0,0 +1,230 @@ +# Veraison stack +AWSTemplateFormatVersion: 2010-09-09 +Description: Veraison attestation verfication services + +Parameters: + # mandatory parameters (no defaults): + DeploymentName: + Type: String + Description: | + The name of this deployment. Defaults to the Cloudfromation stack name + VpcId: + Description: ID for the VPC into which Veraison will be deployed + Type: AWS::EC2::VPC::Id + KeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: | + The name of an EC2 key pair that will be used to provide access to the + instance. + CombinedImage: + Type: String + Description: ID of the AMI image to be used for the instance. + KeycloakImage: + Type: String + Description: ID of the AMI image to be used for the instance. + AdminCidr: + Type: String + Description: CIDR to used to configure remote access + SubnetId: + Type: String + Description: ID of the subnet to be used for veraison deployment + SubnetCidr: + Type: String + Description: CIDR of the subnet identified by SubnetId + + # optional parameters (have a default if are not specfied): + ProvisioningPort: + Type: Number + Description: TCP port on which the provisioning service is listening + Default: 8888 + VerificationPort: + Type: Number + Description: TCP port on which the verification service is listening + Default: 8080 + ManagementPort: + Type: Number + Description: TCP port on which the management service is listening + Default: 8088 + KeycloakPort: + Type: Number + Description: TCP port on which the management service is listening + Default: 11111 + ServiceInstanceType: + Description: An EC2 instance type that will be used to run EC2 Instances + Type: String + Default: t2.micro + KeycloakInstanceType: + Description: An EC2 instance type that will be used to run EC2 Instances + Type: String + Default: t2.micro + + RdsSubnets: + Type: String + RdsEngine: + Type: String + Default: postgres + RdsInstanceType: + Type: String + Default: db.t3.micro + RdsAllocatedStorage: + Type: Number + Default: 5 + RdsDb: + Type: String + Default: veraison + RdsUser: + Type: String + Default: veraison + RdsPassword: + Type: String + Default: veraison + +Resources: + + VeraisonSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + VpcId: !Ref VpcId + GroupName: veraison-services + GroupDescription: Veraison services access + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: !Ref AdminCidr + - IpProtocol: tcp + FromPort: !Ref ProvisioningPort + ToPort: !Ref ProvisioningPort + CidrIp: !Ref AdminCidr + - IpProtocol: tcp + FromPort: !Ref VerificationPort + ToPort: !Ref VerificationPort + CidrIp: !Ref AdminCidr + - IpProtocol: tcp + FromPort: !Ref ManagementPort + ToPort: !Ref ManagementPort + CidrIp: !Ref AdminCidr + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + KeycloakSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + VpcId: !Ref VpcId + GroupName: keycloak + GroupDescription: Keycloak access + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: !Ref AdminCidr + - IpProtocol: tcp + FromPort: !Ref KeycloakPort + ToPort: !Ref KeycloakPort + CidrIp: !Ref AdminCidr + - IpProtocol: tcp + FromPort: !Ref KeycloakPort + ToPort: !Ref KeycloakPort + CidrIp: !Ref SubnetCidr + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + VeraisonInstance: + Type: AWS::EC2::Instance + Properties: + KeyName: !Ref KeyName + InstanceType: !Ref ServiceInstanceType + ImageId: !Ref CombinedImage + SubnetId: !Ref SubnetId + SecurityGroupIds: + - !GetAtt VeraisonSecurityGroup.GroupId + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + - Key: deployment-instance-name + Value: combined + + KeycloakInstance: + Type: AWS::EC2::Instance + Properties: + KeyName: !Ref KeyName + InstanceType: !Ref ServiceInstanceType + ImageId: !Ref KeycloakImage + SubnetId: !Ref SubnetId + SecurityGroupIds: + - !GetAtt KeycloakSecurityGroup.GroupId + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + - Key: deployment-instance-name + Value: keycloak + + VeraisonIpAddress: + Type: AWS::EC2::EIP + DependsOn: VeraisonInstance + Properties: + Domain: vpc + InstanceId: !Ref VeraisonInstance + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + KeycloakIpAddress: + Type: AWS::EC2::EIP + DependsOn: VeraisonInstance + Properties: + Domain: vpc + InstanceId: !Ref KeycloakInstance + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + RdsSubnetGroup: + Type: AWS::RDS::DBSubnetGroup + Properties: + DBSubnetGroupName: VeraisonRdsSubnetGroup + DBSubnetGroupDescription: combined stack RDS subnet group + SubnetIds: !Split [",", !Ref RdsSubnets] + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + RdsSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security Group for RDS instances. + VpcId: !Ref VpcId + SecurityGroupIngress: + - CidrIp: !Ref SubnetCidr + FromPort: 5432 + ToPort: 5432 + IpProtocol: TCP + - CidrIp: !Ref AdminCidr + FromPort: 5432 + ToPort: 5432 + IpProtocol: TCP + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + RdsInstance: + Type: AWS::RDS::DBInstance + DependsOn: + - RdsSubnetGroup + - RdsSecurityGroup + Properties: + Engine: !Ref RdsEngine + DBInstanceClass: !Ref RdsInstanceType + AllocatedStorage: !Ref RdsAllocatedStorage + DBName: !Ref RdsDb + MasterUsername: !Ref RdsUser + MasterUserPassword: !Ref RdsPassword + DBSubnetGroupName: VeraisonRdsSubnetGroup + PubliclyAccessible: true + VPCSecurityGroups: + - !Ref RdsSecurityGroup + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName diff --git a/deployments/aws/templates/stack-debug.yaml b/deployments/aws/templates/stack-debug.yaml new file mode 100644 index 00000000..0cf3dfad --- /dev/null +++ b/deployments/aws/templates/stack-debug.yaml @@ -0,0 +1,76 @@ +# Veraison stack +AWSTemplateFormatVersion: 2010-09-09 +Description: Veraison attestation verfication services + +Parameters: + # mandatory parameters (no defaults): + DeploymentName: + Type: String + Description: | + The name of this deployment. + VpcId: + Description: ID for the VPC into which Veraison will be deployed + Type: AWS::EC2::VPC::Id + KeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: | + The name of an EC2 key pair that will be used to provide access to the + instance. + InstanceImage: + Type: String + Description: ID of the AMI image to be used for the instance. + AdminCidr: + Type: String + Description: CIDR to used to configure remote access + SubnetId: + Type: String + Description: ID of the subnet to be used for veraison deployment + + # optional parameters (have a default if are not specfied): + InstanceType: + Description: An EC2 instance type that will be used to run EC2 Instances + Type: String + Default: t2.micro + + +Resources: + + SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + VpcId: !Ref VpcId + GroupName: veraison-test + GroupDescription: Veraison services access + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: !Ref AdminCidr + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + Instance: + Type: AWS::EC2::Instance + Properties: + KeyName: !Ref KeyName + InstanceType: !Ref InstanceType + ImageId: !Ref InstanceImage + SubnetId: !Ref SubnetId + SecurityGroupIds: + - !GetAtt SecurityGroup.GroupId + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + - Key: deployment-instance-name + Value: debug + + IpAddress: + Type: AWS::EC2::EIP + DependsOn: Instance + Properties: + Domain: vpc + InstanceId: !Ref Instance + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName diff --git a/deployments/debian/debian/postinst b/deployments/debian/debian/postinst index 83c5bba0..fdaa232a 100644 --- a/deployments/debian/debian/postinst +++ b/deployments/debian/debian/postinst @@ -22,5 +22,7 @@ if [ "$1" = "configure" ]; then chown -R "$VERAISON_USER":"$VERAISON_GROUP" /opt/veraison/certs chown -R "$VERAISON_USER":"$VERAISON_GROUP" /opt/veraison/stores + chmod 0500 /opt/veraison/certs/*.key + /opt/veraison/bin/veraison -s start-services fi diff --git a/end-to-end/end-to-end-aws b/end-to-end/end-to-end-aws new file mode 100755 index 00000000..9475838e --- /dev/null +++ b/end-to-end/end-to-end-aws @@ -0,0 +1,152 @@ +#!/bin/bash +# Copyright 2024 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +SCHEME=${SCHEME:-psa} +CONFIG_DIR=/tmp/veraison-aws-client-configs + +THIS_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +source "${THIS_DIR}/../deployments/aws/env/env.bash" + +veraison --quiet --deployment-name "${VERAISON_AWS_DEPLOYMENT}" \ + create-client-config --output-dir "$CONFIG_DIR" + +function provision() { + case $SCHEME in + psa) + local corim_file=$THIS_DIR/input/psa-endorsements.cbor + local media_type="application/corim-unsigned+cbor; profile=\"http://arm.com/psa/iot/1\"" + ;; + cca) + local corim_file=$THIS_DIR/input/cca-endorsements.cbor + local media_type="application/corim-unsigned+cbor; profile=\"http://arm.com/cca/ssd/1\"" + ;; + *) + echo "${_error}: bad SCHEME: $SCHEME" + exit 1 + ;; + esac + + set -x + cocli corim submit --config="${CONFIG_DIR}/cocli/config.yaml" \ + --corim-file="$corim_file" --media-type="$media_type" +} + +function check() { + veraison --deployment-name "${VERAISON_AWS_DEPLOYMENT}" stores +} + +function verify_as_attester() { + case $SCHEME in + psa) + local claims="$THIS_DIR/input/psa-claims-profile-2-without-nonce.json" + local key_args="--key=$THIS_DIR/input/ec-p256.jwk" + local nonce_args="--nonce-size=32" + ;; + cca) + local claims="$THIS_DIR/input/cca-claims-without-realm-challenge.json" + local key_args="--iak=$THIS_DIR/input/ec256.json --rak=$THIS_DIR/input/ec384.json" + local nonce_args="" + ;; + *) + echo "${_error}: bad SCHEME: $SCHEME" + exit 1 + ;; + esac + + set -x + evcli "$SCHEME" verify-as attester --config="${CONFIG_DIR}/evcli/config.yaml" \ + --claims="$claims" $key_args $nonce_args +} + +function verify_as_relying_party() { + case $SCHEME in + psa) + local token=$THIS_DIR/input/psa-evidence.cbor + ;; + cca) + local token=$THIS_DIR/input/cca-evidence.cbor + ;; + *) + echo "${_error}: bad SCHEME: $SCHEME" + exit 1 + ;; + esac + + set -x + evcli "$SCHEME" verify-as relying-party --config="${CONFIG_DIR}/evcli/config.yaml" \ + --token="$token" +} + +function verify() { + as=$1 + + case $as in + attester | attest) verify_as_attester;; + rp | relying-party) verify_as_relying_party;; + *) echo "ERROR unexected \"as\": \"$as\"; should be either \"attester\" or \"rp\"" + esac +} + +function help() { +cat <<'EOF' +Usage: ./end-to-end-native [command] + +Command: + + provision + Provision endorsements and trust anchors via the provisioning API. + check | check-stores + Check the contents of the endorsement and trust anchor stores. + verify [ attester | rp ] + Verify claims via verification API either as an attester or the relying party. + +EOF +} + +function _check_scheme() { + case $SCHEME in + psa | cca) + ;; + *) + echo "${_error}: unknown SCHEME: '$SCHEME'; must be 'cca' or 'psa'"; exit 1 + ;; + esac +} + +function _check_installed() { + local what=$1 + + if [[ "$(type -p "$what")" == "" ]]; then + echo -e "$_error: $what executable must be installed to use this command." + exit 1 + fi +} + +_error='\e[0;31mERROR\e[0m' + +while getopts "hs:" opt; do + case "$opt" in + h) help; exit 0;; + s) SCHEME="$OPTARG";; + *) break;; + esac +done + +shift $((OPTIND-1)) +[ "${1:-}" = "--" ] && shift + +_check_scheme +_check_installed cocli +_check_installed evcli + +command=$1 +case $command in + help) help;; + provision) provision;; + check | check-stores) check;; + verify) verify "$2";; + *) echo "${_error}: unexpected command: \"$command\""; help;; +esac +# vim: set et sts=4 sw=4: