1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

enable ssl certificate import for hyperglass-agent

This commit is contained in:
checktheroads
2020-03-21 01:44:38 -07:00
parent cd454d5ca1
commit fe61a7e90f
11 changed files with 224 additions and 34 deletions

View File

@@ -15,11 +15,11 @@ from starlette.middleware.cors import CORSMiddleware
# Project
from hyperglass.util import log
from hyperglass.constants import __version__
from hyperglass.constants import TRANSPORT_REST, __version__
from hyperglass.api.events import on_startup, on_shutdown
from hyperglass.api.routes import docs, query, queries, routers
from hyperglass.api.routes import docs, query, queries, routers, import_certificate
from hyperglass.exceptions import HyperglassError
from hyperglass.configuration import URL_DEV, STATIC_PATH, params
from hyperglass.configuration import URL_DEV, STATIC_PATH, params, devices
from hyperglass.api.error_handlers import (
app_handler,
http_handler,
@@ -200,6 +200,18 @@ app.add_api_route(
response_class=UJSONResponse,
)
# Enable certificate import route only if a device using
# hyperglass-agent is defined.
for device in devices.routers:
if device.nos in TRANSPORT_REST:
app.add_api_route(
path="/api/import-agent-certificate/",
endpoint=import_certificate,
methods=["POST"],
include_in_schema=False,
)
break
if params.docs.enable:
app.add_api_route(path=params.docs.uri, endpoint=docs, include_in_schema=False)
app.openapi = _custom_openapi

View File

@@ -0,0 +1,14 @@
"""hyperglass-agent certificate import models."""
# Standard Library
from typing import Union
# Third Party
from pydantic import BaseModel, StrictStr
# Project
from hyperglass.configuration.models._utils import StrictBytes
class EncodedRequest(BaseModel):
device: StrictStr
encoded: Union[StrictStr, StrictBytes]

View File

@@ -1,6 +1,7 @@
"""API Routes."""
# Standard Library
import os
import time
# Third Party
@@ -10,14 +11,18 @@ from starlette.requests import Request
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
# Project
from hyperglass.util import log
from hyperglass.util import log, clean_name, import_public_key
from hyperglass.encode import jwt_decode
from hyperglass.exceptions import HyperglassError
from hyperglass.configuration import REDIS_CONFIG, params, devices
from hyperglass.api.models.query import Query
from hyperglass.execution.execute import Execute
from hyperglass.api.models.cert_import import EncodedRequest
Cache = aredis.StrictRedis(db=params.cache.database, **REDIS_CONFIG)
APP_PATH = os.environ["hyperglass_directory"]
async def query(query_data: Query, request: Request):
"""Ingest request data pass it to the backend application to perform the query."""
@@ -60,6 +65,46 @@ async def query(query_data: Query, request: Request):
return {"output": cache_response, "level": "success", "keywords": []}
async def import_certificate(encoded_request: EncodedRequest):
"""Import a certificate from hyperglass-agent."""
# Try to match the requested device name with configured devices
matched_device = None
requested_device_name = clean_name(encoded_request.device)
for device in devices.routers:
if device.name == requested_device_name:
matched_device = device
break
if matched_device is None:
raise HTTPException(
detail=f"Device {str(encoded_request.device)} not found", status_code=404
)
try:
# Decode JSON Web Token
decoded_request = await jwt_decode(
payload=encoded_request.encoded,
secret=matched_device.credential.password.get_secret_value(),
)
except HyperglassError as decode_error:
raise HTTPException(detail=str(decode_error), status_code=401)
try:
# Write certificate to file
import_public_key(
app_path=APP_PATH, device_name=device.name, keystring=decoded_request
)
except RuntimeError as import_error:
raise HyperglassError(str(import_error), level="danger")
return {
"output": f"Added public key for {encoded_request.device}",
"level": "success",
"keywords": [encoded_request.device],
}
async def docs():
"""Serve custom docs."""
if params.docs.enable:

View File

@@ -8,23 +8,8 @@ from pathlib import Path
# Third Party
from pydantic import HttpUrl, BaseModel
def clean_name(_name):
"""Remove unsupported characters from field names.
Converts any "desirable" seperators to underscore, then removes all
characters that are unsupported in Python class variable names.
Also removes leading numbers underscores.
Arguments:
_name {str} -- Initial field name
Returns:
{str} -- Cleaned field name
"""
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name)
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
return _scrubbed.lower()
# Project
from hyperglass.util import clean_name
class HyperglassModel(BaseModel):
@@ -107,6 +92,49 @@ class AnyUri(str):
return f"AnyUri({super().__repr__()})"
class StrictBytes(bytes):
"""Custom data type for a strict byte string.
Used for validating the encoded JWT request payload.
"""
@classmethod
def __get_validators__(cls):
"""Yield Pydantic validator function.
See: https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types
Yields:
{function} -- Validator
"""
yield cls.validate
@classmethod
def validate(cls, value):
"""Validate type.
Arguments:
value {Any} -- Pre-validated input
Raises:
TypeError: Raised if value is not bytes
Returns:
{object} -- Instantiated class
"""
if not isinstance(value, bytes):
raise TypeError("bytes required")
return cls()
def __repr__(self):
"""Return representation of object.
Returns:
{str} -- Representation
"""
return f"StrictBytes({super().__repr__()})"
def validate_image(value):
"""Convert file path to URL path.

View File

@@ -4,7 +4,8 @@
from pydantic import SecretStr
# Project
from hyperglass.configuration.models._utils import HyperglassModel, clean_name
from hyperglass.util import clean_name
from hyperglass.configuration.models._utils import HyperglassModel
class Credential(HyperglassModel):

View File

@@ -1,12 +1,11 @@
"""Validate network configuration variables."""
# Third Party
# Third Party Imports
from pydantic import Field, StrictStr
# Project
# Project Imports
from hyperglass.configuration.models._utils import HyperglassModel, clean_name
from hyperglass.util import clean_name
from hyperglass.configuration.models._utils import HyperglassModel
class Network(HyperglassModel):

View File

@@ -4,8 +4,9 @@
from pydantic import StrictInt, StrictStr, validator
# Project
from hyperglass.util import clean_name
from hyperglass.exceptions import UnsupportedDevice
from hyperglass.configuration.models._utils import HyperglassModel, clean_name
from hyperglass.configuration.models._utils import HyperglassModel
from hyperglass.configuration.models.credentials import Credential

View File

@@ -1,23 +1,21 @@
"""Validate router configuration variables."""
# Standard Library
import os
import re
from typing import List, Optional
from pathlib import Path
# Third Party
from pydantic import StrictInt, StrictStr, validator
# Project
from hyperglass.util import log
from hyperglass.util import log, clean_name
from hyperglass.constants import Supported
from hyperglass.exceptions import ConfigError, UnsupportedDevice
from hyperglass.configuration.models.ssl import Ssl
from hyperglass.configuration.models.vrfs import Vrf, Info
from hyperglass.configuration.models._utils import (
HyperglassModel,
HyperglassModelExtra,
clean_name,
)
from hyperglass.configuration.models._utils import HyperglassModel, HyperglassModelExtra
from hyperglass.configuration.models.proxies import Proxy
from hyperglass.configuration.models.commands import Command
from hyperglass.configuration.models.networks import Network
@@ -72,7 +70,7 @@ class Router(HyperglassModel):
return value
@validator("name")
def clean_name(cls, value):
def validate_name(cls, value):
"""Remove or replace unsupported characters from field values.
Arguments:
@@ -83,6 +81,27 @@ class Router(HyperglassModel):
"""
return clean_name(value)
@validator("ssl")
def validate_ssl(cls, value, values):
"""Set default cert file location if undefined.
Arguments:
value {object} -- SSL object
values {dict} -- Other already-valiated fields
Returns:
{object} -- SSL configuration
"""
if value is not None:
if value.enable and value.cert is None:
app_path = Path(os.environ["hyperglass_directory"])
cert_file = app_path / "certs" / f'{values["name"]}.pem'
if not cert_file.exists():
log.warning("No certificate found for device {d}", d=values["name"])
cert_file.touch()
value.cert = cert_file
return value
@validator("commands", always=True)
def validate_commands(cls, value, values):
"""If a named command profile is not defined, use the NOS name.

View File

@@ -23,6 +23,7 @@ from netmiko import (
# Project
from hyperglass.util import log
from hyperglass.encode import jwt_decode, jwt_encode
from hyperglass.constants import Supported
from hyperglass.exceptions import (
AuthError,
@@ -32,7 +33,6 @@ from hyperglass.exceptions import (
ResponseEmpty,
)
from hyperglass.configuration import params, devices
from hyperglass.execution.encode import jwt_decode, jwt_encode
from hyperglass.execution.construct import Construct
@@ -258,6 +258,14 @@ class Connect:
"timeout": params.request_timeout,
}
if self.device.ssl is not None and self.device.ssl.enable:
with self.device.ssl.cert.open("r") as file:
cert = file.read()
if not cert:
raise RestError(
"SSL Certificate for device {d} has not been imported",
level="danger",
d=self.device.display_name,
)
http_protocol = "https"
client_params.update({"verify": str(self.device.ssl.cert)})
log.debug(

View File

@@ -27,6 +27,26 @@ def cpu_count():
return multiprocessing.cpu_count()
def clean_name(_name):
"""Remove unsupported characters from field names.
Converts any "desirable" seperators to underscore, then removes all
characters that are unsupported in Python class variable names.
Also removes leading numbers underscores.
Arguments:
_name {str} -- Initial field name
Returns:
{str} -- Cleaned field name
"""
import re
_replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name)
_scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced))
return _scrubbed.lower()
async def check_path(path, mode="r"):
"""Verify if a path exists and is accessible.
@@ -512,3 +532,46 @@ def set_app_path(required=False):
os.environ["hyperglass_directory"] = str(matched_path)
return True
def import_public_key(app_path, device_name, keystring):
"""Import a public key for hyperglass-agent.
Arguments:
app_path {Path|str} -- hyperglass app path
device_name {str} -- Device name
keystring {str} -- Public key
Raises:
RuntimeError: Raised if unable to create certs directory
RuntimeError: Raised if written key does not match input
Returns:
{bool} -- True if file was written
"""
import re
from pathlib import Path
if not isinstance(app_path, Path):
app_path = Path(app_path)
cert_dir = app_path / "certs"
if not cert_dir.exists():
cert_dir.mkdir()
if not cert_dir.exists():
raise RuntimeError(f"Failed to create certs directory at {str(cert_dir)}")
filename = re.sub(r"[^A-Za-z0-9]", "_", device_name) + ".pem"
cert_file = cert_dir / filename
with cert_file.open("w+") as file:
file.write(str(keystring))
with cert_file.open("r") as file:
read_file = file.read().strip()
if not keystring == read_file:
raise RuntimeError("Wrote key, but written file did not match input key")
return True