Esempio n. 1
0
def create_service_account(namespace: str) -> object:
    enforce_authorization(namespace)

    f = open("templates/keycloak/client.json", "r")
    j = json.loads(f.read())

    cid = "sa-%s-%s" % (namespace, get_random_string(10))

    j['clientId'] = cid
    j['protocolMappers'][0]['config']['claim.value'] = namespace

    keycloak_admin = admin_api()

    try:
        response = keycloak_admin.create_client(j)
        cuuid = keycloak_admin.get_client_id(cid)
        r = keycloak_admin.generate_client_secrets(cuuid)
        return ({'client_id': cid, 'client_secret': r['value']}, 201)
    except KeycloakGetError as err:
        if err.response_code == 409:
            abort(
                make_response(
                    jsonify(
                        error=
                        "Service Account for this namespace is already created."
                    ), 400))
        else:
            log.error(err)
            abort(
                make_response(jsonify(error="Failed to add service account"),
                              400))
Esempio n. 2
0
def update_service_account_credentials(namespace: str,
                                       client_id: str) -> object:
    enforce_authorization(namespace)

    cid = "sa-%s-" % namespace

    if not client_id.startswith(cid):
        abort(make_response(jsonify(error="Invalid client ID"), 400))

    keycloak_admin = admin_api()

    try:
        cuuid = keycloak_admin.get_client_id(client_id)
        r = keycloak_admin.generate_client_secrets(cuuid)
        return ({'client_id': client_id, 'client_secret': r['value']}, 201)
    except KeycloakGetError as err:
        if err.response_code == 409:
            abort(
                make_response(
                    jsonify(
                        error=
                        "Service Account for this namespace is already created."
                    ), 400))
        else:
            log.error(err)
            abort(
                make_response(jsonify(error="Failed to add service account"),
                              400))
Esempio n. 3
0
def update_namespace(namespace: str) -> object:
    log = app.logger
    enforce_authorization(namespace)

    params = request.get_json(force=True)

    if not namespace_valid(namespace):
        log.error("Namespace validation failed %s", namespace)
        abort(
            make_response(
                jsonify(
                    error=
                    "Namespace name validation failed.  Reference regular expression '%s'."
                    % namespace_validation_rule), 400))

    try:
        svc = NamespaceService()

        ns_group = svc.get_namespace(namespace)

        svc.update_ns_attributes(ns_group, params)

    except KeycloakGetError as err:
        log.error("Failed to update namespace %s", namespace)
        log.error(err)
        abort(make_response(jsonify(error="Failed to update namespace"), 400))

    return make_response(jsonify())
Esempio n. 4
0
def update_membership(namespace: str) -> object:
    # Sync the membership list provided with the group membership
    # in Keycloak
    #
    enforce_authorization(namespace)

    desired_membership_list = json.loads(request.get_data())

    ucounts_added, ucounts_removed, ucounts_missing = membership_sync(
        namespace, 'viewer', desired_membership_list)
    acounts_added, acounts_removed, acounts_missing = membership_sync(
        namespace, 'admin', desired_membership_list)

    return make_response(
        jsonify(added=ucounts_added + acounts_added,
                removed=ucounts_removed + acounts_removed,
                missing=ucounts_missing + acounts_missing))
Esempio n. 5
0
def delete_namespace(namespace: str) -> object:
    log = app.logger
    enforce_authorization(namespace)
    enforce_role_authorization('aps.ns:manage')

    keycloak_admin = admin_api()

    try:
        for role_name in ['viewer', 'admin']:
            group = keycloak_admin.get_group_by_path(
                "%s/%s" % (get_base_group_path(role_name), namespace),
                search_in_subgroups=True)
            if group is not None:
                keycloak_admin.delete_group(group['id'])
    except KeycloakGetError as err:
        log.error(err)
        abort(make_response(jsonify(error="Failed to delete namespace"), 400))

    return ('', 204)
Esempio n. 6
0
def list_service_accounts(namespace: str) -> object:
    enforce_authorization(namespace)

    keycloak_admin = admin_api()

    try:
        params_path = {"realm-name": keycloak_admin.realm_name}
        data_raw = keycloak_admin.raw_get(
            URL_ADMIN_CLIENTS.format(**params_path),
            clientId='sa-%s-' % namespace,
            search=True)
        response = raise_error_from_response(data_raw, KeycloakGetError)
        result = []
        for r in response:
            result.append(r['clientId'])
        return (json.dumps(result), 200)
    except KeycloakGetError as err:
        log.error(err)
        abort(
            make_response(jsonify(error="Failed to read service accounts"),
                          400))
Esempio n. 7
0
def delete_service_account(namespace: str, client_id: str) -> object:
    log = app.logger
    enforce_authorization(namespace)

    if client_id_valid(namespace, client_id) == False:
        abort(make_response(jsonify(error="Invalid client ID"), 400))

    keycloak_admin = admin_api()

    try:
        cuuid = keycloak_admin.get_client_id(client_id)
        if cuuid is None:
            abort(
                make_response(jsonify(error="Service Account does not exist"),
                              400))
        else:
            keycloak_admin.delete_client(cuuid)
            return ({}, 204)
    except KeycloakGetError as err:
        log.error(err)
        abort(
            make_response(jsonify(error="Failed to delete service account"),
                          400))
Esempio n. 8
0
def get_statuses(namespace: str) -> object:
    enforce_authorization(namespace)

    log = app.logger

    log.info("Get status for %s" % namespace)

    services = get_services_by_ns(namespace)
    routes = get_routes_by_ns(namespace)

    response = []

    for service in services:
        url = build_url(service)
        status = "UP"
        reason = ""

        actual_host = None
        host = None
        for route in routes:
            if route['service']['id'] == service['id'] and 'hosts' in route:
                actual_host = route['hosts'][0]
                host = clean_host(actual_host)

        try:
            addr = socket.gethostbyname(service['host'])
            log.info("Address = %s" % addr)
        except:
            status = "DOWN"
            reason = "DNS"

        if status == "UP":
            try:
                headers = {}
                if host is None or service['host'].endswith('.svc'):
                    r = requests.get(url, headers=headers, timeout=3.0)
                    status_code = r.status_code
                else:
                    u = urlparse(url)

                    headers['Host'] = host
                    log.info("GET %-30s %s" % ("%s://%s" %
                                               (u.scheme, u.netloc), headers))

                    urllib3.disable_warnings()
                    if u.scheme == "https":
                        pool = urllib3.HTTPSConnectionPool(
                            "%s" % (u.netloc),
                            assert_hostname=host,
                            server_hostname=host,
                            cert_reqs='CERT_NONE',
                            ca_certs=certifi.where())
                    else:
                        pool = urllib3.HTTPConnectionPool("%s" % (u.netloc))
                    req = pool.urlopen("GET",
                                       u.path,
                                       headers={"Host": host},
                                       assert_same_host=False,
                                       timeout=1.0,
                                       retries=False)

                    status_code = req.status

                log.info("Result received!! %d" % status_code)
                if status_code < 400:
                    status = "UP"
                    reason = "%d Response" % status_code
                elif status_code == 401 or status_code == 403:
                    status = "UP"
                    reason = "AUTH %d" % status_code
                else:
                    status = "DOWN"
                    reason = "%d Response" % status_code
            except requests.exceptions.Timeout as ex:
                status = "DOWN"
                reason = "TIMEOUT"
            except urllib3.exceptions.ConnectTimeoutError as ex:
                status = "DOWN"
                reason = "TIMEOUT"
            except requests.exceptions.ConnectionError as ex:
                log.error("ConnError %s" % ex)
                status = "DOWN"
                reason = "CONNECTION"
            except requests.exceptions.SSLError as ex:
                status = "DOWN"
                reason = "SSL"
            except urllib3.exceptions.NewConnectionError as ex:
                log.error("NewConnError %s" % ex)
                status = "DOWN"
                reason = "CON_ERR"
            except urllib3.exceptions.SSLError as ex:
                log.error(ex)
                status = "DOWN"
                reason = "SSL_URLLIB3"
            except Exception as ex:
                log.error(ex)
                traceback.print_exc(file=sys.stdout)
                status = "DOWN"
                reason = "UNKNOWN"

        log.info("GET %-30s %s" % (url, reason))
        response.append({
            "name": service['name'],
            "upstream": url,
            "status": status,
            "reason": reason,
            "host": host,
            "env_host": actual_host
        })

    return make_response(jsonify(response))
Esempio n. 9
0
def delete_config(namespace: str, qualifier = "") -> object:
    enforce_authorization(namespace)

    event_id = str(uuid.uuid4())
    record_gateway_event(event_id, 'delete', 'received', namespace)

    log = app.logger

    outFolder = namespace

    tempFolder = "%s/%s/%s" % ('/tmp', uuid.uuid4(), outFolder)
    os.makedirs (tempFolder, exist_ok=False)

    with open("%s/%s" % (tempFolder, 'empty.yaml'), 'w') as file:
        file.write("")

    selectTag = "ns.%s" % namespace
    log.debug("ST = %s" % selectTag)
    if qualifier is not None and qualifier != "":
        log.debug("What is qual? %s" % qualifier)
        selectTag = "ns.%s.%s" % (namespace, qualifier)
    
    # Call the 'deck' command
    cmd = "sync"

    log.info("[%s] %s action using %s" % (namespace, cmd, selectTag))
    args = [
        "deck", cmd, "--config", "/tmp/deck.yaml", "--skip-consumers", "--select-tag", selectTag, "--state", tempFolder
    ]
    log.debug("[%s] Running %s" % (namespace, args))
    deck_run = Popen(args, stdout=PIPE, stderr=STDOUT)
    out, err = deck_run.communicate()
    if deck_run.returncode != 0:
        cleanup (tempFolder)
        log.warn("%s - %s" % (namespace, out.decode('utf-8')))
        abort_early(event_id, 'delete', namespace, jsonify(error="Sync Failed.", results=mask(out.decode('utf-8'))) )

    elif cmd == "sync":
        try:
            route_count = prepare_apply_routes (namespace, selectTag, is_host_transform_enabled(), tempFolder)
            log.debug("%s - Prepared %d routes" % (namespace, route_count))
            if route_count > 0:
                apply_routes (tempFolder)
                log.debug("%s - Applied %d routes" % (namespace, route_count))
            route_count = prepare_delete_routes (namespace, selectTag, tempFolder)
            log.debug("%s - Prepared %d deletions" % (namespace, route_count))
            if route_count > 0:
                delete_routes (tempFolder)
        
            # create Network Security Policies (nsp) for any upstream that
            # has the format: <name>.<ocp_ns>.svc
            log.debug("%s - Update NSPs" % (namespace))
            ocp_ns_list = get_ocp_service_namespaces (tempFolder)
            for ocp_ns in ocp_ns_list:
                if check_nsp (namespace, ocp_ns) is False:
                    apply_nsp (namespace, ocp_ns, tempFolder)

            # ok all looks good, so update a secret containing the original submitted request
            log.debug("%s - Update Original Config" % (namespace))
            write_submitted_config ("", tempFolder)
            prep_and_apply_secret (namespace, selectTag, tempFolder)
            log.debug("%s - Updated Original Config" % (namespace))
        except HTTPException as ex:
            traceback.print_exc()
            log.error("Error updating custom routes, nsps and secrets. %s" % ex)
            abort_early(event_id, 'delete', namespace, jsonify(error="Partially failed.") )
        except:
            traceback.print_exc()
            log.error("Error updating custom routes, nsps and secrets. %s" % sys.exc_info()[0])
            abort_early(event_id, 'delete', namespace, jsonify(error="Partially failed.") )

    cleanup (tempFolder)

    log.debug("[%s] The exit code was: %d" % (namespace, deck_run.returncode))

    message = "Sync successful."
    if cmd == 'diff':
        message = "Dry-run.  No changes applied."

    record_gateway_event(event_id, 'delete', 'completed', namespace)
    return make_response('', http.HTTPStatus.NO_CONTENT)
Esempio n. 10
0
def write_config(namespace: str) -> object:
    """
    (Over)write
    :return: JSON of success message or error message
    """
    enforce_authorization(namespace)

    event_id = str(uuid.uuid4())
    record_gateway_event(event_id, 'publish', 'received', namespace)

    log = app.logger

    outFolder = namespace

    # Build a list of existing hosts that are outside this namespace
    # They become reserved and any conflict will return an error
    reserved_hosts = []
    all_routes = get_routes()
    tag_match = "ns.%s" % namespace
    for route in all_routes:
        if tag_match not in route['tags'] and 'hosts' in route:
            for host in route['hosts']:
                reserved_hosts.append(transform_host(host))
    reserved_hosts = list(set(reserved_hosts))

    ns_svc = NamespaceService()
    ns_attributes = ns_svc.get_namespace_attributes (namespace)

    dfile = None

    if 'configFile' in request.files:
        log.debug("[%s] %s", namespace, request.files['configFile'])
        dfile = request.files['configFile']
        dry_run = request.values['dryRun']
    elif request.content_type.startswith("application/json"):
        dfile = request.json['configFile']
        dry_run = request.json['dryRun']
    else:
        log.error("Missing input")
        log.error("%s", request.get_data())
        log.error(request.form)
        log.error(request.content_type)
        log.error(request.headers)
        abort_early(event_id, 'publish', namespace, jsonify(error="Missing input"))

    tempFolder = "%s/%s/%s" % ('/tmp', uuid.uuid4(), outFolder)
    os.makedirs (tempFolder, exist_ok=False)

    # dfile.save("%s/%s" % (tempFolder, 'config.yaml'))
    
    # log.debug("Saved to %s" % tempFolder)
    yaml_documents_iter = yaml.load_all(dfile, Loader=yaml.FullLoader)

    yaml_documents = []
    for doc in yaml_documents_iter:
        yaml_documents.append(doc)

    selectTag = "ns.%s" % namespace
    ns_qualifier = None

    orig_config = prep_submitted_config (yaml_documents)

    update_routes_flag = False

    if len(yaml_documents) == 0:
        update_routes_flag = True

    for index, gw_config in enumerate(yaml_documents):
        log.info("[%s] Parsing file %s" % (namespace, index))

        if gw_config is None:
            continue

        #######################
        # Enrichments
        #######################

        # Transformation route hosts if in non-prod environment (HOST_TRANSFORM_ENABLED)
        host_transformation (namespace, gw_config)

        # If there is a tag with a pipeline qualifier (i.e./ ns.<namespace>.dev)
        # then add to tags automatically the tag: ns.<namespace>
        tags_transformation (namespace, gw_config)

        #
        # Enrich the rate-limiting plugin with the appropriate Redis details
        plugins_transformations (namespace, gw_config)

        with open("%s/%s" % (tempFolder, 'config-%02d.yaml' % index), 'w') as file:
            yaml.dump(gw_config, file)

        #######################
        # Validations
        #######################

        # Validate that the every object is tagged with the namespace
        try:
            validate_tags (gw_config, selectTag)
        except Exception as ex:
            traceback.print_exc()
            log.error("%s - %s" % (namespace, " Tag Validation Errors: %s" % ex))
            abort_early(event_id, 'publish', namespace, jsonify(error="Validation Errors:\n%s" % ex))

        # Validate that hosts are valid
        try:
            validate_hosts (gw_config, reserved_hosts, ns_attributes)
        except Exception as ex:
            traceback.print_exc()
            log.error("%s - %s" % (namespace, " Host Validation Errors: %s" % ex))
            abort_early(event_id, 'publish', namespace, jsonify(error="Validation Errors:\n%s" % ex))

        # Validate upstream URLs are valid
        try:
            protected_kube_namespaces = json.loads(app.config['protectedKubeNamespaces'])
            validate_upstream (gw_config, ns_attributes, protected_kube_namespaces)
        except Exception as ex:
            traceback.print_exc()
            log.error("%s - %s" % (namespace, " Upstream Validation Errors: %s" % ex))
            abort_early(event_id, 'publish', namespace, jsonify(error="Validation Errors:\n%s" % ex))

        # Validation #3
        # Validate that certain plugins are configured (such as the gwa_gov_endpoint) at the right level

        # Validate based on DNS 952
    
        nsq = traverse_get_ns_qualifier (gw_config, selectTag)
        if nsq is not None:
            if ns_qualifier is not None and nsq != ns_qualifier:
                abort_early(event_id, 'publish', namespace, jsonify(error="Validation Errors:\n%s" % ("Conflicting ns qualifiers (%s != %s)" % (ns_qualifier, nsq))))
            ns_qualifier = nsq
            log.info("[%s] CHANGING ns_qualifier %s" % (namespace, ns_qualifier))

        if update_routes_check(gw_config):
            update_routes_flag = True

    if ns_qualifier is not None:
        selectTag = ns_qualifier

    # Call the 'deck' command
    cmd = "sync"
    if dry_run == 'true' or dry_run is True:
        cmd = "diff"

    log.info("[%s] %s action using %s" % (namespace, cmd, selectTag))
    args = [
        "deck", cmd, "--config", "/tmp/deck.yaml", "--skip-consumers", "--select-tag", selectTag, "--state", tempFolder
    ]
    log.debug("[%s] Running %s" % (namespace, args))
    deck_run = Popen(args, stdout=PIPE, stderr=STDOUT)
    out, err = deck_run.communicate()
    if deck_run.returncode != 0:
        cleanup (tempFolder)
        log.warn("[%s] - %s" % (namespace, out.decode('utf-8')))
        abort_early(event_id, 'publish', namespace, jsonify(error="Sync Failed.", results=mask(out.decode('utf-8'))))

    elif cmd == "sync":
        try:
            if update_routes_flag:
                route_count = prepare_apply_routes (namespace, selectTag, is_host_transform_enabled(), tempFolder)
                log.debug("[%s] - Prepared %d routes" % (namespace, route_count))
                if route_count > 0:
                    apply_routes (tempFolder)
                    log.debug("[%s] - Applied %d routes" % (namespace, route_count))
                route_count = prepare_delete_routes (namespace, selectTag, tempFolder)
                log.debug("[%s] - Prepared %d deletions" % (namespace, route_count))
                if route_count > 0:
                    delete_routes (tempFolder)
            
            # create Network Security Policies (nsp) for any upstream that
            # has the format: <name>.<ocp_ns>.svc
            if should_we_apply_nsp_policies():
                log.debug("[%s] - Update NSPs" % (namespace))
                ocp_ns_list = get_ocp_service_namespaces (tempFolder)
                for ocp_ns in ocp_ns_list:
                    if check_nsp (namespace, ocp_ns) is False:
                        apply_nsp (namespace, ocp_ns, tempFolder)

            # ok all looks good, so update a secret containing the original submitted request
            log.debug("[%s] - Update Original Config" % (namespace))
            write_submitted_config (orig_config, tempFolder)
            prep_and_apply_secret (namespace, selectTag, tempFolder)
            log.debug("[%s] - Updated Original Config" % (namespace))
        except HTTPException as ex:
            traceback.print_exc()
            log.error("[%s] Error updating custom routes, nsps and secrets. %s" % (namespace, ex))
            abort_early(event_id, 'publish', namespace, jsonify(error="Partially failed."))
        except:
            traceback.print_exc()
            log.error("[%s] Error updating custom routes, nsps and secrets. %s" % (namespace, sys.exc_info()[0]))
            abort_early(event_id, 'publish', namespace, jsonify(error="Partially failed."))

    cleanup (tempFolder)

    log.debug("[%s] The exit code was: %d" % (namespace, deck_run.returncode))

    message = "Sync successful."
    if cmd == 'diff':
        message = "Dry-run.  No changes applied."

    record_gateway_event(event_id, 'publish', 'completed', namespace)
    return make_response(jsonify(message=message, results=mask(out.decode('utf-8'))))