mirror of https://github.com/devine-dl/devine.git
Add custom RESTful Vault API Interface
This commit is contained in:
parent
c08c45fc16
commit
e967c7c8d1
28
CONFIG.md
28
CONFIG.md
|
@ -188,12 +188,28 @@ provide the same Key ID and CEK for both Video and Audio, as well as for multipl
|
||||||
You can have as many Key Vaults as you would like. It's nice to share Key Vaults or use a unified Vault on
|
You can have as many Key Vaults as you would like. It's nice to share Key Vaults or use a unified Vault on
|
||||||
Teams as sharing CEKs immediately can help reduce License calls drastically.
|
Teams as sharing CEKs immediately can help reduce License calls drastically.
|
||||||
|
|
||||||
Two types of Vaults are in the Core codebase, SQLite and MySQL Vaults. Both directly connect to an SQLite or MySQL
|
Three types of Vaults are in the Core codebase, API, SQLite and MySQL. API makes HTTP requests to a RESTful API,
|
||||||
Server. It has to connect directly to the Host/IP. It cannot be in front of a PHP API or such. Beware that some Hosts
|
whereas SQLite and MySQL directly connect to an SQLite or MySQL Database.
|
||||||
do not let you access the MySQL server outside their intranet (aka Don't port forward or use permissive network
|
|
||||||
interfaces).
|
|
||||||
|
|
||||||
### Connecting to a MySQL Vault
|
Note: SQLite and MySQL vaults have to connect directly to the Host/IP. It cannot be in front of a PHP API or such.
|
||||||
|
Beware that some Hosting Providers do not let you access the MySQL server outside their intranet and may not be
|
||||||
|
accessible outside their hosting platform.
|
||||||
|
|
||||||
|
### Using an API Vault
|
||||||
|
|
||||||
|
API vaults use a specific HTTP request format, therefore API or HTTP Key Vault APIs from other projects or services may
|
||||||
|
not work in Devine. The API format can be seen in the [API Vault Code](devine/vaults/API.py).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- type: API
|
||||||
|
name: "John#0001's Vault" # arbitrary vault name
|
||||||
|
uri: "https://key-vault.example.com" # api base uri (can also be an IP or IP:Port)
|
||||||
|
# uri: "127.0.0.1:80/key-vault"
|
||||||
|
# uri: "https://api.example.com/key-vault"
|
||||||
|
token: "random secret key" # authorization token
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using a MySQL Vault
|
||||||
|
|
||||||
MySQL vaults can be either MySQL or MariaDB servers. I recommend MariaDB.
|
MySQL vaults can be either MySQL or MariaDB servers. I recommend MariaDB.
|
||||||
A MySQL Vault can be on a local or remote network, but I recommend SQLite for local Vaults.
|
A MySQL Vault can be on a local or remote network, but I recommend SQLite for local Vaults.
|
||||||
|
@ -219,7 +235,7 @@ make tables yourself.
|
||||||
- You may give trusted users CREATE permission so devine can create tables if needed.
|
- You may give trusted users CREATE permission so devine can create tables if needed.
|
||||||
- Other uses should only be given SELECT and INSERT permissions.
|
- Other uses should only be given SELECT and INSERT permissions.
|
||||||
|
|
||||||
### Connecting to an SQLite Vault
|
### Using an SQLite Vault
|
||||||
|
|
||||||
SQLite Vaults are usually only used for locally stored vaults. This vault may be stored on a mounted Cloud storage
|
SQLite Vaults are usually only used for locally stored vaults. This vault may be stored on a mounted Cloud storage
|
||||||
drive, but I recommend using SQLite exclusively as an offline-only vault. Effectively this is your backup vault in
|
drive, but I recommend using SQLite exclusively as an offline-only vault. Effectively this is your backup vault in
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
from typing import Iterator, Optional, Union
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
from devine.core import __version__
|
||||||
|
from devine.core.vault import Vault
|
||||||
|
|
||||||
|
|
||||||
|
class API(Vault):
|
||||||
|
"""Key Vault using a simple RESTful HTTP API call."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, uri: str, token: str):
|
||||||
|
super().__init__(name)
|
||||||
|
self.uri = uri.rstrip("/")
|
||||||
|
self.session = Session()
|
||||||
|
self.session.headers.update({
|
||||||
|
"User-Agent": f"Devine v{__version__}"
|
||||||
|
})
|
||||||
|
self.session.headers.update({
|
||||||
|
"Authorization": f"Bearer {token}"
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
||||||
|
if isinstance(kid, UUID):
|
||||||
|
kid = kid.hex
|
||||||
|
|
||||||
|
data = self.session.get(
|
||||||
|
url=f"{self.uri}/{service.lower()}/{kid}",
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
).json()
|
||||||
|
|
||||||
|
code = int(data.get("code", 0))
|
||||||
|
message = data.get("message")
|
||||||
|
error = {
|
||||||
|
0: None,
|
||||||
|
1: Exceptions.AuthRejected,
|
||||||
|
2: Exceptions.TooManyRequests,
|
||||||
|
3: Exceptions.ServiceTagInvalid,
|
||||||
|
4: Exceptions.KeyIdInvalid
|
||||||
|
}.get(code, ValueError)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise error(f"{message} ({code})")
|
||||||
|
|
||||||
|
content_key = data.get("content_key")
|
||||||
|
if not content_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(content_key, str):
|
||||||
|
raise ValueError(f"Expected {content_key} to be {str}, was {type(content_key)}")
|
||||||
|
|
||||||
|
return content_key
|
||||||
|
|
||||||
|
def get_keys(self, service: str) -> Iterator[tuple[str, str]]:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data = self.session.get(
|
||||||
|
url=f"{self.uri}/{service.lower()}",
|
||||||
|
params={
|
||||||
|
"page": page,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
).json()
|
||||||
|
|
||||||
|
code = int(data.get("code", 0))
|
||||||
|
message = data.get("message")
|
||||||
|
error = {
|
||||||
|
0: None,
|
||||||
|
1: Exceptions.AuthRejected,
|
||||||
|
2: Exceptions.TooManyRequests,
|
||||||
|
3: Exceptions.PageInvalid,
|
||||||
|
4: Exceptions.ServiceTagInvalid,
|
||||||
|
}.get(code, ValueError)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise error(f"{message} ({code})")
|
||||||
|
|
||||||
|
content_keys = data.get("content_keys")
|
||||||
|
if content_keys:
|
||||||
|
if not isinstance(content_keys, dict):
|
||||||
|
raise ValueError(f"Expected {content_keys} to be {dict}, was {type(content_keys)}")
|
||||||
|
|
||||||
|
for key_id, key in content_keys.items():
|
||||||
|
yield key_id, key
|
||||||
|
|
||||||
|
pages = int(data["pages"])
|
||||||
|
if pages <= page:
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
def add_key(self, service: str, kid: Union[UUID, str], key: str) -> bool:
|
||||||
|
if isinstance(kid, UUID):
|
||||||
|
kid = kid.hex
|
||||||
|
|
||||||
|
data = self.session.post(
|
||||||
|
url=f"{self.uri}/{service.lower()}/{kid}",
|
||||||
|
json={
|
||||||
|
"content_key": key
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
).json()
|
||||||
|
|
||||||
|
code = int(data.get("code", 0))
|
||||||
|
message = data.get("message")
|
||||||
|
error = {
|
||||||
|
0: None,
|
||||||
|
1: Exceptions.AuthRejected,
|
||||||
|
2: Exceptions.TooManyRequests,
|
||||||
|
3: Exceptions.ServiceTagInvalid,
|
||||||
|
4: Exceptions.KeyIdInvalid,
|
||||||
|
5: Exceptions.ContentKeyInvalid
|
||||||
|
}.get(code, ValueError)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise error(f"{message} ({code})")
|
||||||
|
|
||||||
|
# the kid:key was new to the vault (optional)
|
||||||
|
added = bool(data.get("added"))
|
||||||
|
# the key for kid was changed/updated (optional)
|
||||||
|
updated = bool(data.get("updated"))
|
||||||
|
|
||||||
|
return added or updated
|
||||||
|
|
||||||
|
def add_keys(self, service: str, kid_keys: dict[Union[UUID, str], str]) -> int:
|
||||||
|
data = self.session.post(
|
||||||
|
url=f"{self.uri}/{service.lower()}",
|
||||||
|
json={
|
||||||
|
"content_keys": {
|
||||||
|
str(kid).replace("-", ""): key
|
||||||
|
for kid, key in kid_keys.items()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
).json()
|
||||||
|
|
||||||
|
code = int(data.get("code", 0))
|
||||||
|
message = data.get("message")
|
||||||
|
error = {
|
||||||
|
0: None,
|
||||||
|
1: Exceptions.AuthRejected,
|
||||||
|
2: Exceptions.TooManyRequests,
|
||||||
|
3: Exceptions.ServiceTagInvalid,
|
||||||
|
4: Exceptions.KeyIdInvalid,
|
||||||
|
5: Exceptions.ContentKeyInvalid
|
||||||
|
}.get(code, ValueError)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise error(f"{message} ({code})")
|
||||||
|
|
||||||
|
# each kid:key that was new to the vault (optional)
|
||||||
|
added = int(data.get("added"))
|
||||||
|
# each key for a kid that was changed/updated (optional)
|
||||||
|
updated = int(data.get("updated"))
|
||||||
|
|
||||||
|
return added + updated
|
||||||
|
|
||||||
|
def get_services(self) -> Iterator[str]:
|
||||||
|
data = self.session.post(
|
||||||
|
url=self.uri,
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
).json()
|
||||||
|
|
||||||
|
code = int(data.get("code", 0))
|
||||||
|
message = data.get("message")
|
||||||
|
error = {
|
||||||
|
0: None,
|
||||||
|
1: Exceptions.AuthRejected,
|
||||||
|
2: Exceptions.TooManyRequests,
|
||||||
|
}.get(code, ValueError)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise error(f"{message} ({code})")
|
||||||
|
|
||||||
|
service_list = data.get("service_list", [])
|
||||||
|
|
||||||
|
if not isinstance(service_list, list):
|
||||||
|
raise ValueError(f"Expected {service_list} to be {list}, was {type(service_list)}")
|
||||||
|
|
||||||
|
for service in service_list:
|
||||||
|
yield service
|
||||||
|
|
||||||
|
|
||||||
|
class Exceptions:
|
||||||
|
class AuthRejected(Exception):
|
||||||
|
"""Authentication Error Occurred, is your token valid? Do you have permission to make this call?"""
|
||||||
|
|
||||||
|
class TooManyRequests(Exception):
|
||||||
|
"""Rate Limited; Sent too many requests in a given amount of time."""
|
||||||
|
|
||||||
|
class PageInvalid(Exception):
|
||||||
|
"""Requested page does not exist."""
|
||||||
|
|
||||||
|
class ServiceTagInvalid(Exception):
|
||||||
|
"""The Service Tag is invalid."""
|
||||||
|
|
||||||
|
class KeyIdInvalid(Exception):
|
||||||
|
"""The Key ID is invalid."""
|
||||||
|
|
||||||
|
class ContentKeyInvalid(Exception):
|
||||||
|
"""The Content Key is invalid."""
|
Loading…
Reference in New Issue