Browse Source

use separate token auth for sensei admin api from node service

patch-1
John Cantrell 3 years ago
parent
commit
5715c57d08
  1. 19
      README.md
  2. 38
      proto/sensei.proto
  3. 203
      src/database/admin.rs
  4. 232
      src/grpc/admin.rs
  5. 286
      src/http/admin.rs
  6. 22
      src/http/auth_header.rs
  7. 2
      src/http/mod.rs
  8. 38
      src/http/node.rs
  9. 9
      src/main.rs
  10. 86
      src/services/admin.rs
  11. 8
      src/utils.rs

19
README.md

@ -1,6 +1,5 @@
![Sensei Logo](./web-admin/public/images/sensei-logo.svg)
### **WARNING: This software is in beta. Do not use it on mainnet until this warning is removed. Expect breaking changes to the api and database schema until the 0.1.0 release.**
<br/>
@ -9,9 +8,9 @@ Sensei is a new lightning node implementation with a focus on easing the onboard
## Dependencies
At the moment you will need Bitcoind and an Electrum server to use Sensei. More flexible backend options are coming.
You will need a bitcoind instance to use Sensei.
I recommend using [Nigiri](https://nigiri.vulpem.com/) to get both services running locally.
I recommend using [Nigiri](https://nigiri.vulpem.com/) to get everything running locally.
## Building and running from source
@ -19,10 +18,9 @@ To run from source you will need to take the following steps:
1. Clone the repo: `git clone git@github.com:L2-Technology/sensei.git`
2. Build the web-admin: `cd sensei/web-admin && npm install && npm run build && cd ..`
3. Run senseid on regtest: `cargo run --bin senseid -- --network=regtest --electrum-url=localhost:50000`
3. Run senseid on regtest: `cargo run --bin senseid -- --network=regtest --bitcoind-rpc-host=localhost --bitcoind-rpc-port=18443 --bitcoind-rpc-username=admin1 --bitcoind-rpc-password=123`
4. Open the admin at `http://localhost:5401/admin/nodes`
## Developing the web-admin
In order to see your changes live you will need to:
@ -32,7 +30,7 @@ In order to see your changes live you will need to:
## Using with Nigiri
[Nigiri](https://nigiri.vulpem.com/) is a great tool for running local docker images of bitcoind, electrum, esplora, and much more. Once it's running you can use `localhost:50000` as your `Electrum URL` when setting up your Sensei node.
[Nigiri](https://nigiri.vulpem.com/) is a great tool for running local docker images of bitcoind, electrum, esplora, and much more. Once it's running you can use the bitcoind instance it provides when starting up your Sensei node.
Once your node is setup you can:
@ -42,12 +40,6 @@ Once your node is setup you can:
- Getting an address to mine to `nigiri rpc getnewaddress "" "bech32"`
- Mine some blocks to that address `nigiri rpc generatetoaddress 10 "<address_from_previous_command>"`
## Other Development Notes
Currently the on-chain wallet is only sycned once every 30 seconds in the background. This means after you fund your wallet or open channels it can take up to 30 seconds for the changes to be reflected in Sensei admin. You'll also need to navigate or refresh the page.
I'm hoping to fix this asap.
## Data directory
You can pass a custom data directory using --data_dir flag but the default will be a `.sensei` directory in your operating systems home directory.
@ -55,7 +47,7 @@ You can pass a custom data directory using --data_dir flag but the default will
Home directory is retrieved using the [dirs crate](https://github.com/dirs-dev/dirs-rs).
| Platform | Value | Example |
| ------- | -------------------- | ---------------------- |
| -------- | -------------------- | ---------------------- |
| Linux | `$HOME` | /home/alice/.sensei |
| macOS | `$HOME` | /Users/Alice/.sensei |
| Windows | `{FOLDERID_Profile}` | C:\Users\Alice\.sensei |
@ -72,7 +64,6 @@ Home directory is retrieved using the [dirs crate](https://github.com/dirs-dev/d
This function retrieves the user profile folder using `SHGetKnownFolderPath`.
## Configuration Files
Sensei will create a root `config.json` file inside the data directory. These are configurations that will be applied across all networks.

38
proto/sensei.proto

@ -10,6 +10,9 @@ service Admin {
rpc GetStatus (GetStatusRequest) returns (GetStatusResponse);
rpc StartNode (AdminStartNodeRequest) returns (AdminStartNodeResponse);
rpc StopNode (AdminStopNodeRequest) returns (AdminStopNodeResponse);
rpc ListTokens (ListTokensRequest) returns (ListTokensResponse);
rpc CreateToken (CreateTokenRequest) returns (Token);
rpc DeleteToken (DeleteTokenRequest) returns (DeleteTokenResponse);
}
service Node {
@ -47,6 +50,18 @@ message ListNode {
uint32 status = 12;
}
message Token {
uint64 id = 1;
string external_id = 2;
string created_at = 3;
string updated_at = 4;
uint64 expires_at = 5;
string name = 6;
string token = 7;
bool single_use = 8;
string scope = 9;
}
message PaginationRequest {
uint32 page = 1;
uint32 take = 3;
@ -67,6 +82,15 @@ message ListNodesResponse {
PaginationResponse pagination = 2;
}
message ListTokensRequest {
optional PaginationRequest pagination = 1;
}
message ListTokensResponse {
repeated Token tokens = 1;
PaginationResponse pagination = 2;
}
message CreateAdminRequest {
string username = 1;
string alias = 2;
@ -79,6 +103,7 @@ message CreateAdminResponse {
string macaroon = 2;
string external_id = 3;
uint32 role = 4;
string token = 5;
}
message CreateNodeRequest {
@ -97,12 +122,25 @@ message DeleteNodeRequest {
}
message DeleteNodeResponse {}
message CreateTokenRequest {
string name = 1;
string scope = 2;
uint64 expires_at = 3;
bool single_use = 4;
}
message DeleteTokenRequest {
uint64 id = 1;
}
message DeleteTokenResponse {}
message StartAdminRequest {
string passphrase = 1;
}
message StartAdminResponse {
string pubkey = 1;
string macaroon = 2;
string token = 3;
}
message GetStatusRequest {}

203
src/database/admin.rs

@ -7,8 +7,15 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use std::time::SystemTime;
use super::Error;
use crate::services::{PaginationRequest, PaginationResponse};
use crate::utils::{self, seconds_since_epoch};
use crate::{
hex_utils,
services::{PaginationRequest, PaginationResponse},
};
use rand::{thread_rng, Rng};
use rusqlite::{named_params, Connection};
use serde::Serialize;
use uuid::Uuid;
@ -119,6 +126,60 @@ impl Node {
}
}
#[derive(Debug, Serialize, PartialEq, Clone)]
pub struct AccessToken {
pub id: u64,
pub name: String,
pub external_id: String,
pub scope: String,
pub token: String,
pub expires_at: u64,
pub single_use: bool,
pub created_at: String,
pub updated_at: String,
}
impl AccessToken {
pub fn new(name: String, scope: String, expires_at: u64, single_use: bool) -> Self {
let mut token_bytes: [u8; 32] = [0; 32];
thread_rng().fill_bytes(&mut token_bytes);
let token = hex_utils::hex_str(&token_bytes);
Self {
id: 0,
name,
external_id: Uuid::new_v4().to_string(),
token,
scope,
expires_at,
single_use,
created_at: "".to_string(),
updated_at: "".to_string(),
}
}
pub fn new_admin() -> Self {
Self::new("admin".to_string(), "*".to_string(), 0, false)
}
pub fn is_expired(&self) -> bool {
self.expires_at != 0 && utils::seconds_since_epoch().unwrap() > self.expires_at
}
pub fn has_access_to_scope(&self, scope: Option<&str>) -> bool {
match scope {
Some(scope) => {
let scopes: Vec<&str> = self.scope.split(",").collect();
scopes.contains(&"*") || scopes.contains(&scope)
}
None => true,
}
}
pub fn is_valid(&self, scope: Option<&str>) -> bool {
!self.is_expired() && self.has_access_to_scope(scope)
}
}
static MIGRATIONS: &[&str] = &[
"CREATE TABLE version (version INTEGER)",
"INSERT INTO version VALUES (1)",
@ -128,6 +189,7 @@ static MIGRATIONS: &[&str] = &[
"CREATE UNIQUE INDEX idx_external_id ON nodes(external_id)",
"CREATE UNIQUE INDEX idx_pubkey ON nodes(pubkey)",
"CREATE INDEX idx_role ON nodes(role)",
"CREATE TABLE access_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, external_id TEXT NOT NULL, name TEXT, token TEXT, scope TEXT, expires_at INTEGER DEFAULT 0, single_use INTEGER DEFAULT 0, created_at INTEGER NOT NULL DEFAULT current_timestamp, updated_at INTEGER NOT NULL DEFAULT current_timestamp)"
];
pub struct AdminDatabase {
@ -147,6 +209,145 @@ impl AdminDatabase {
}
impl AdminDatabase {
pub fn create_access_token(&mut self, access_token: &AccessToken) -> Result<i64, Error> {
let mut statement = self.connection.prepare_cached("INSERT INTO access_tokens (external_id, name, token, scope, expires_at, single_use) VALUES (:external_id, :name, :token, :scope, :expires_at, :single_use)")?;
statement.execute(named_params! {
":external_id": access_token.external_id,
":name": access_token.name,
":token": access_token.token,
":scope": access_token.scope,
":expires_at": access_token.expires_at,
":single_use": access_token.single_use
})?;
Ok(self.connection.last_insert_rowid())
}
pub fn delete_access_token(&mut self, id: u64) -> Result<(), Error> {
let mut statement = self
.connection
.prepare_cached("DELETE FROM access_tokens WHERE id=:id")?;
statement.execute(named_params! { ":id": id})?;
Ok(())
}
pub fn get_admin_access_token(&mut self) -> Result<Option<AccessToken>, Error> {
self.get_access_token_by_scope("*".to_string())
}
pub fn get_access_token_by_scope(
&mut self,
scope: String,
) -> Result<Option<AccessToken>, Error> {
let mut statement = self.connection.prepare_cached(
"SELECT id, external_id, name, created_at, updated_at, token, scope, expires_at, single_use FROM access_tokens WHERE scope=:scope"
)?;
let mut rows = statement.query(named_params! { ":scope": scope })?;
match rows.next()? {
Some(row) => Ok(Some(AccessToken {
id: row.get(0)?,
external_id: row.get(1)?,
name: row.get(2)?,
created_at: row.get(3)?,
updated_at: row.get(4)?,
token: row.get(5)?,
scope: row.get(6)?,
expires_at: row.get(7)?,
single_use: row.get(8)?,
})),
None => Ok(None),
}
}
pub fn get_access_token(&mut self, token: String) -> Result<Option<AccessToken>, Error> {
let mut statement = self.connection.prepare_cached(
"SELECT id, external_id, name, created_at, updated_at, token, scope, expires_at, single_use FROM access_tokens WHERE token=:token",
)?;
let mut rows = statement.query(named_params! { ":token": token })?;
match rows.next()? {
Some(row) => Ok(Some(AccessToken {
id: row.get(0)?,
external_id: row.get(1)?,
name: row.get(2)?,
created_at: row.get(3)?,
updated_at: row.get(4)?,
token: row.get(5)?,
scope: row.get(6)?,
expires_at: row.get(7)?,
single_use: row.get(8)?,
})),
None => Ok(None),
}
}
pub fn list_access_tokens(
&mut self,
pagination: PaginationRequest,
) -> Result<(Vec<AccessToken>, PaginationResponse), Error> {
let query_string = pagination.query.unwrap_or_else(|| String::from(""));
let mut count_statement = self
.connection
.prepare("SELECT COUNT(1) as cnt FROM access_tokens WHERE instr(access_tokens.token, :query) > 0")?;
let count = count_statement.query_row(
named_params! {
":query": query_string
},
|row| {
let count = row.get(0).unwrap_or(0);
Ok(count as u64)
},
)?;
let mut statement = self.connection.prepare(
"
SELECT id, external_id, name, created_at, updated_at, token, scope, expires_at, single_use
FROM access_tokens
WHERE instr(access_tokens.token, :query) > 0
ORDER BY access_tokens.updated_at DESC
LIMIT :take
OFFSET :offset
",
)?;
let mut rows = statement.query(named_params! {
":offset": pagination.page * pagination.take,
":take": pagination.take + 1,
":query": query_string
})?;
let mut tokens = Vec::new();
while let Some(row) = rows.next()? {
tokens.push(AccessToken {
id: row.get(0)?,
external_id: row.get(1)?,
name: row.get(2)?,
created_at: row.get(3)?,
updated_at: row.get(4)?,
token: row.get(5)?,
scope: row.get(6)?,
expires_at: row.get(7)?,
single_use: row.get(8)?,
})
}
let has_more = tokens.len() > pagination.take as usize;
if has_more {
tokens.pop();
}
let pagination = PaginationResponse {
has_more,
total: count,
};
Ok((tokens, pagination))
}
pub fn create_node(&mut self, node: Node) -> Result<i64, Error> {
let mut statement = self.connection.prepare_cached("INSERT INTO nodes (external_id, username, alias, role, network, listen_addr, listen_port, pubkey, status) VALUES (:external_id, :username, :alias, :role, :network, :listen_addr, :listen_port, :pubkey, :status)")?;

232
src/grpc/admin.rs

@ -13,15 +13,34 @@ pub use super::sensei::admin_server::{Admin, AdminServer};
use super::sensei::{
AdminStartNodeRequest, AdminStartNodeResponse, AdminStopNodeRequest, AdminStopNodeResponse,
CreateAdminRequest, CreateAdminResponse, CreateNodeRequest, CreateNodeResponse,
DeleteNodeRequest, DeleteNodeResponse, GetStatusRequest, GetStatusResponse, ListNode,
ListNodesRequest, ListNodesResponse, StartAdminRequest, StartAdminResponse,
CreateTokenRequest, DeleteNodeRequest, DeleteNodeResponse, DeleteTokenRequest,
DeleteTokenResponse, GetStatusRequest, GetStatusResponse, ListNode, ListNodesRequest,
ListNodesResponse, ListTokensRequest, ListTokensResponse, StartAdminRequest,
StartAdminResponse, Token,
};
use crate::{
database::admin::AccessToken,
services::admin::{AdminRequest, AdminResponse},
utils,
};
use tonic::{metadata::MetadataMap, Response, Status};
impl From<AccessToken> for Token {
fn from(access_token: AccessToken) -> Token {
Token {
id: access_token.id,
external_id: access_token.external_id,
created_at: access_token.created_at,
updated_at: access_token.updated_at,
expires_at: access_token.expires_at,
token: access_token.token,
name: access_token.name,
single_use: access_token.single_use,
scope: access_token.scope,
}
}
}
impl From<ListNodesRequest> for AdminRequest {
fn from(req: ListNodesRequest) -> Self {
AdminRequest::ListNodes {
@ -60,6 +79,31 @@ impl TryFrom<AdminResponse> for ListNodesResponse {
}
}
impl From<ListTokensRequest> for AdminRequest {
fn from(req: ListTokensRequest) -> Self {
AdminRequest::ListTokens {
pagination: req.pagination.into(),
}
}
}
impl TryFrom<AdminResponse> for ListTokensResponse {
type Error = String;
fn try_from(res: AdminResponse) -> Result<Self, Self::Error> {
match res {
AdminResponse::ListTokens { tokens, pagination } => Ok(Self {
tokens: tokens
.into_iter()
.map(|token| token.into())
.collect::<Vec<Token>>(),
pagination: Some(pagination.into()),
}),
_ => Err("impossible".to_string()),
}
}
}
impl From<CreateNodeRequest> for AdminRequest {
fn from(req: CreateNodeRequest) -> Self {
AdminRequest::CreateNode {
@ -82,6 +126,28 @@ impl TryFrom<AdminResponse> for CreateNodeResponse {
}
}
impl From<CreateTokenRequest> for AdminRequest {
fn from(req: CreateTokenRequest) -> Self {
AdminRequest::CreateToken {
name: req.name,
scope: req.scope,
expires_at: req.expires_at,
single_use: req.single_use,
}
}
}
impl TryFrom<AdminResponse> for Token {
type Error = String;
fn try_from(res: AdminResponse) -> Result<Self, Self::Error> {
match res {
AdminResponse::CreateToken { token } => Ok(token.into()),
_ => Err("impossible".to_string()),
}
}
}
impl From<CreateAdminRequest> for AdminRequest {
fn from(req: CreateAdminRequest) -> Self {
AdminRequest::CreateAdmin {
@ -103,11 +169,13 @@ impl TryFrom<AdminResponse> for CreateAdminResponse {
macaroon,
external_id,
role,
token,
} => Ok(Self {
pubkey,
macaroon,
external_id,
role: role as u32,
token,
}),
_ => Err("impossible".to_string()),
}
@ -154,7 +222,15 @@ impl TryFrom<AdminResponse> for StartAdminResponse {
fn try_from(res: AdminResponse) -> Result<Self, Self::Error> {
match res {
AdminResponse::StartAdmin { pubkey, macaroon } => Ok(Self { pubkey, macaroon }),
AdminResponse::StartAdmin {
pubkey,
macaroon,
token,
} => Ok(Self {
pubkey,
macaroon,
token,
}),
_ => Err("impossible".to_string()),
}
}
@ -213,75 +289,93 @@ impl TryFrom<AdminResponse> for DeleteNodeResponse {
}
}
}
impl From<DeleteTokenRequest> for AdminRequest {
fn from(req: DeleteTokenRequest) -> Self {
AdminRequest::DeleteToken { id: req.id }
}
}
impl TryFrom<AdminResponse> for DeleteTokenResponse {
type Error = String;
fn try_from(res: AdminResponse) -> Result<Self, Self::Error> {
match res {
AdminResponse::DeleteToken {} => Ok(Self {}),
_ => Err("impossible".to_string()),
}
}
}
pub struct AdminService {
pub request_context: Arc<crate::RequestContext>,
}
impl AdminService {
async fn authenticated_request(
&self,
metadata: MetadataMap,
request: AdminRequest,
) -> Result<AdminResponse, tonic::Status> {
let macaroon_hex_string = self.raw_macaroon_from_metadata(metadata)?;
let (macaroon, session) =
utils::macaroon_with_session_from_hex_str(&macaroon_hex_string)
.map_err(|_e| tonic::Status::unauthenticated("invalid macaroon"))?;
let pubkey = session.pubkey.clone();
pub fn get_scope_from_request(request: &AdminRequest) -> Option<&'static str> {
match request {
AdminRequest::CreateNode { .. } => Some("nodes/create"),
AdminRequest::ListNodes { .. } => Some("nodes/list"),
AdminRequest::DeleteNode { .. } => Some("nodes/delete"),
AdminRequest::StopNode { .. } => Some("nodes/stop"),
_ => None,
}
}
let admin_node = {
impl AdminService {
async fn is_valid_token(&self, token: String, scope: Option<&str>) -> bool {
let access_token = {
let mut admin_database = self.request_context.admin_service.database.lock().await;
admin_database
.get_admin_node()
.map_err(|_e| tonic::Status::unknown("database error"))?
admin_database.get_access_token(token)
};
match admin_node {
Some(node) => {
if node.pubkey != pubkey {
return Err(Status::unauthenticated("invalid macaroon"));
match access_token {
Ok(Some(access_token)) => {
if access_token.is_valid(scope) {
if access_token.single_use {
let mut database = self.request_context.admin_service.database.lock().await;
database.delete_access_token(access_token.id).unwrap();
}
true
} else {
false
}
}
Ok(None) => false,
Err(_) => false,
}
}
let node_directory = self.request_context.node_directory.lock().await;
match node_directory.get(&session.pubkey) {
Some(handle) => {
handle
.node
.verify_macaroon(macaroon, session)
.await
.map_err(|_e| {
Status::unauthenticated("invalid macaroon: failed to verify")
})?;
async fn authenticated_request(
&self,
metadata: MetadataMap,
request: AdminRequest,
) -> Result<AdminResponse, tonic::Status> {
let required_scope = get_scope_from_request(&request);
drop(node_directory);
let token = self.raw_token_from_metadata(metadata)?;
if self.is_valid_token(token, required_scope).await {
self.request_context
.admin_service
.call(request)
.await
.map_err(|_e| Status::unknown("error"))
}
None => Err(Status::not_found("node with that pubkey not found")),
}
}
None => Err(Status::not_found("admin node has not been created yet")),
} else {
Err(Status::not_found("invalid or expired access token"))
}
}
fn raw_macaroon_from_metadata(&self, metadata: MetadataMap) -> Result<String, tonic::Status> {
let macaroon = metadata.get("macaroon");
fn raw_token_from_metadata(&self, metadata: MetadataMap) -> Result<String, tonic::Status> {
let token = metadata.get("token");
if macaroon.is_none() {
return Err(Status::unauthenticated("macaroon is required"));
if token.is_none() {
return Err(Status::unauthenticated("token is required"));
}
macaroon
token
.unwrap()
.to_str()
.map(String::from)
.map_err(|_e| Status::unauthenticated("invalid macaroon: must be ascii"))
.map_err(|_e| Status::unauthenticated("invalid token: must be ascii"))
}
}
@ -291,19 +385,10 @@ impl Admin for AdminService {
&self,
request: tonic::Request<GetStatusRequest>,
) -> Result<tonic::Response<GetStatusResponse>, tonic::Status> {
let pubkey = {
match self.raw_macaroon_from_metadata(request.metadata().clone()) {
Ok(macaroon_hex_string) => {
match utils::macaroon_with_session_from_hex_str(&macaroon_hex_string) {
Ok((_macaroon, session)) => session.pubkey,
Err(_e) => String::from(""),
}
}
Err(_e) => String::from(""),
}
let token = self.raw_token_from_metadata(request.metadata().clone())?;
let request = AdminRequest::GetStatus {
authenticated: self.is_valid_token(token, None).await,
};
let request = AdminRequest::GetStatus { pubkey };
match self.request_context.admin_service.call(request).await {
Ok(response) => {
let response: Result<GetStatusResponse, String> = response.try_into();
@ -395,4 +480,35 @@ impl Admin for AdminService {
.map(Response::new)
.map_err(|_e| Status::unknown("unknown error"))
}
async fn list_tokens(
&self,
request: tonic::Request<ListTokensRequest>,
) -> Result<tonic::Response<ListTokensResponse>, tonic::Status> {
self.authenticated_request(request.metadata().clone(), request.into_inner().into())
.await?
.try_into()
.map(Response::new)
.map_err(|_e| Status::unknown("unknown error"))
}
async fn create_token(
&self,
request: tonic::Request<CreateTokenRequest>,
) -> Result<tonic::Response<Token>, tonic::Status> {
self.authenticated_request(request.metadata().clone(), request.into_inner().into())
.await?
.try_into()
.map(Response::new)
.map_err(|_e| Status::unknown("unknown error"))
}
async fn delete_token(
&self,
request: tonic::Request<DeleteTokenRequest>,
) -> Result<tonic::Response<DeleteTokenResponse>, tonic::Status> {
self.authenticated_request(request.metadata().clone(), request.into_inner().into())
.await?
.try_into()
.map(Response::new)
.map_err(|_e| Status::unknown("unknown error"))
}
}

286
src/http/admin.rs

@ -11,7 +11,7 @@ use std::sync::Arc;
use axum::{
extract::{Extension, Query},
routing::{get, post},
routing::{delete, get, post},
Json, Router,
};
use tower_cookies::{Cookie, Cookies};
@ -28,7 +28,7 @@ use crate::{
utils, RequestContext,
};
use super::macaroon_header::MacaroonHeader;
use super::auth_header::AuthHeader;
#[derive(Deserialize)]
pub struct DeleteNodeParams {
@ -96,6 +96,36 @@ impl From<CreateNodeParams> for AdminRequest {
}
}
#[derive(Deserialize)]
pub struct CreateTokenParams {
pub name: String,
pub expires_at: u64,
pub scope: String,
pub single_use: bool,
}
impl From<CreateTokenParams> for AdminRequest {
fn from(params: CreateTokenParams) -> Self {
Self::CreateToken {
name: params.name,
expires_at: params.expires_at,
scope: params.scope,
single_use: params.single_use,
}
}
}
#[derive(Deserialize)]
pub struct DeleteTokenParams {
pub id: u64,
}
impl From<DeleteTokenParams> for AdminRequest {
fn from(params: DeleteTokenParams) -> Self {
Self::DeleteToken { id: params.id }
}
}
#[derive(Deserialize)]
pub struct CreateAdminParams {
pub username: String,
@ -128,22 +158,23 @@ impl From<StartAdminParams> for AdminRequest {
}
}
pub fn get_macaroon_hex_str_from_cookies_or_header(
pub fn get_token_from_cookies_or_header(
cookies: &Cookies,
macaroon: Option<HeaderValue>,
token: Option<HeaderValue>,
) -> Result<String, StatusCode> {
match macaroon {
Some(macaroon) => {
let res = macaroon
match token {
Some(token) => {
let res = token
.to_str()
.map(|str| str.to_string())
.map_err(|_| StatusCode::UNAUTHORIZED);
println!("{:?}", res);
res
}
None => match cookies.get("macaroon") {
Some(macaroon_cookie) => {
let macaroon_cookie_str = macaroon_cookie.value().to_string();
Ok(macaroon_cookie_str)
None => match cookies.get("token") {
Some(token_cookie) => {
let token_cookie_str = token_cookie.value().to_string();
Ok(token_cookie_str)
}
None => Err(StatusCode::UNAUTHORIZED),
},
@ -152,46 +183,31 @@ pub fn get_macaroon_hex_str_from_cookies_or_header(
pub async fn authenticate_request(
request_context: &RequestContext,
scope: &str,
cookies: &Cookies,
macaroon: Option<HeaderValue>,
token: Option<HeaderValue>,
) -> Result<bool, StatusCode> {
let macaroon_hex_string = get_macaroon_hex_str_from_cookies_or_header(cookies, macaroon)?;
let token = get_token_from_cookies_or_header(&cookies, token)?;
let (macaroon, session) = utils::macaroon_with_session_from_hex_str(&macaroon_hex_string)
.map_err(|_e| StatusCode::UNAUTHORIZED)?;
let pubkey = session.pubkey.clone();
let admin_node = {
let mut admin_database = request_context.admin_service.database.lock().await;
admin_database
.get_admin_node()
.map_err(|_e| StatusCode::UNAUTHORIZED)?
};
match admin_node {
Some(admin_node) => {
if admin_node.pubkey != pubkey {
return Ok(false);
let access_token = {
let mut database = request_context.admin_service.database.lock().await;
database.get_access_token(token)
}
let node_directory = request_context.node_directory.lock().await;
let node = node_directory.get(&session.pubkey);
match node {
Some(handle) => {
handle
.node
.verify_macaroon(macaroon, session)
.await
.map_err(|_e| StatusCode::UNAUTHORIZED)?;
Ok(true)
match access_token {
Some(access_token) => {
if access_token.is_valid(Some(scope)) {
if access_token.single_use {
let mut database = request_context.admin_service.database.lock().await;
database.delete_access_token(access_token.id).unwrap();
}
None => Ok(false),
Ok(true)
} else {
Ok(false)
}
}
None => Ok(false),
None => return Ok(false),
}
}
@ -203,19 +219,97 @@ pub fn add_routes(router: Router) -> Router {
.route("/v1/nodes/start", post(start_node))
.route("/v1/nodes/stop", post(stop_node))
.route("/v1/nodes/delete", post(delete_node))
.route("/v1/tokens", get(list_tokens))
.route("/v1/tokens", post(create_token))
.route("/v1/tokens", delete(delete_token))
.route("/v1/status", get(get_status))
.route("/v1/start", post(start_sensei))
.route("/v1/login", post(login))
.route("/v1/logout", post(logout))
}
pub async fn list_tokens(
Extension(request_context): Extension<Arc<RequestContext>>,
cookies: Cookies,
Query(pagination): Query<PaginationRequest>,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated =
authenticate_request(&request_context, "tokens/list", &cookies, token).await?;
if authenticated {
match request_context
.admin_service
.call(AdminRequest::ListTokens { pagination })
.await
{
Ok(response) => Ok(Json(response)),
Err(_err) => Err(StatusCode::UNAUTHORIZED),
}
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
pub async fn create_token(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
cookies: Cookies,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated =
authenticate_request(&request_context, "tokens/create", &cookies, token).await?;
let request = {
let params: Result<CreateTokenParams, _> = serde_json::from_value(payload);
match params {
Ok(params) => Ok(params.into()),
Err(_) => Err(StatusCode::UNPROCESSABLE_ENTITY),
}
}?;
if authenticated {
match request_context.admin_service.call(request).await {
Ok(response) => Ok(Json(response)),
Err(_err) => Err(StatusCode::UNAUTHORIZED),
}
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
pub async fn delete_token(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
cookies: Cookies,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated =
authenticate_request(&request_context, "tokens/delete", &cookies, token).await?;
let request = {
let params: Result<DeleteTokenParams, _> = serde_json::from_value(payload);
match params {
Ok(params) => Ok(params.into()),
Err(_) => Err(StatusCode::UNPROCESSABLE_ENTITY),
}
}?;
if authenticated {
match request_context.admin_service.call(request).await {
Ok(response) => Ok(Json(response)),
Err(_err) => Err(StatusCode::UNAUTHORIZED),
}
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
pub async fn list_nodes(
Extension(request_context): Extension<Arc<RequestContext>>,
cookies: Cookies,
Query(pagination): Query<PaginationRequest>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated = authenticate_request(&request_context, &cookies, macaroon).await?;
let authenticated =
authenticate_request(&request_context, "nodes/list", &cookies, token).await?;
if authenticated {
match request_context
.admin_service
@ -247,9 +341,14 @@ pub async fn login(
match node {
Some(node) => {
let request = AdminRequest::StartNode {
let request = match node.is_admin() {
true => AdminRequest::StartAdmin {
passphrase: params.passphrase,
},
false => AdminRequest::StartNode {
pubkey: node.pubkey.clone(),
passphrase: params.passphrase,
},
};
match request_context.admin_service.call(request).await {
@ -267,6 +366,28 @@ pub async fn login(
"role": node.role
})))
}
AdminResponse::StartAdmin {
pubkey,
macaroon,
token,
} => {
let macaroon_cookie = Cookie::build("macaroon", macaroon.clone())
.domain("localhost")
.http_only(true)
.finish();
cookies.add(macaroon_cookie);
let token_cookie = Cookie::build("token", token.clone())
.domain("localhost")
.http_only(true)
.finish();
cookies.add(token_cookie);
Ok(Json(json!({
"pubkey": node.pubkey,
"alias": node.alias,
"macaroon": macaroon,
"role": node.role,
})))
}
_ => Err(StatusCode::UNPROCESSABLE_ENTITY),
},
Err(_err) => Err(StatusCode::UNPROCESSABLE_ENTITY),
@ -278,6 +399,7 @@ pub async fn login(
pub async fn logout(cookies: Cookies) -> Result<Json<Value>, StatusCode> {
cookies.remove(Cookie::new("macaroon", ""));
cookies.remove(Cookie::new("token", ""));
Ok(Json::default())
}
@ -299,17 +421,26 @@ pub async fn init_sensei(
macaroon,
external_id,
role,
token,
} => {
let macaroon_cookie = Cookie::build("macaroon", macaroon.clone())
.domain("localhost")
.http_only(true)
.finish();
let token_cookie = Cookie::build("token", token.clone())
.domain("localhost")
.http_only(true)
.finish();
cookies.add(macaroon_cookie);
cookies.add(token_cookie);
Ok(Json(AdminResponse::CreateAdmin {
pubkey,
macaroon,
external_id,
role,
token,
}))
}
_ => Err(StatusCode::UNPROCESSABLE_ENTITY),
@ -321,23 +452,26 @@ pub async fn init_sensei(
pub async fn get_status(
Extension(request_context): Extension<Arc<RequestContext>>,
cookies: Cookies,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let pubkey = {
match get_macaroon_hex_str_from_cookies_or_header(&cookies, macaroon) {
Ok(macaroon_hex_string) => {
match utils::macaroon_with_session_from_hex_str(&macaroon_hex_string) {
Ok((_macaroon, session)) => session.pubkey,
Err(_e) => String::from(""),
}
}
Err(_e) => String::from(""),
}
let token = match get_token_from_cookies_or_header(&cookies, token) {
Ok(token) => token,
Err(e) => String::from(""),
};
let admin_token = {
let mut database = request_context.admin_service.database.lock().await;
database.get_admin_access_token().unwrap()
};
let authenticated = match admin_token {
Some(access_token) => access_token.token == token,
None => false,
};
match request_context
.admin_service
.call(AdminRequest::GetStatus { pubkey })
.call(AdminRequest::GetStatus { authenticated })
.await
{
Ok(response) => Ok(Json(response)),
@ -364,14 +498,28 @@ pub async fn start_sensei(
.await
{
Ok(response) => match response {
AdminResponse::StartAdmin { pubkey, macaroon } => {
AdminResponse::StartAdmin {
pubkey,
macaroon,
token,
} => {
let macaroon_cookie = Cookie::build("macaroon", macaroon.clone())
.domain("localhost")
.http_only(true)
.permanent()
.finish();
cookies.add(macaroon_cookie);
Ok(Json(AdminResponse::StartAdmin { pubkey, macaroon }))
let token_cookie = Cookie::build("token", token.clone())
.domain("localhost")
.http_only(true)
.permanent()
.finish();
cookies.add(token_cookie);
Ok(Json(AdminResponse::StartAdmin {
pubkey,
macaroon,
token,
}))
}
_ => Err(StatusCode::UNPROCESSABLE_ENTITY),
},
@ -386,9 +534,10 @@ pub async fn create_node(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
cookies: Cookies,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated = authenticate_request(&request_context, &cookies, macaroon).await?;
let authenticated =
authenticate_request(&request_context, "nodes/create", &cookies, token).await?;
let request = {
let params: Result<CreateNodeParams, _> = serde_json::from_value(payload);
match params {
@ -411,9 +560,10 @@ pub async fn start_node(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
cookies: Cookies,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated = authenticate_request(&request_context, &cookies, macaroon).await?;
let authenticated =
authenticate_request(&request_context, "nodes/start", &cookies, token).await?;
let request = {
let params: Result<StartNodeParams, _> = serde_json::from_value(payload);
match params {
@ -436,9 +586,10 @@ pub async fn stop_node(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
cookies: Cookies,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated = authenticate_request(&request_context, &cookies, macaroon).await?;
let authenticated =
authenticate_request(&request_context, "nodes/stop", &cookies, token).await?;
let request = {
let params: Result<StopNodeParams, _> = serde_json::from_value(payload);
match params {
@ -461,9 +612,10 @@ pub async fn delete_node(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
cookies: Cookies,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token }: AuthHeader,
) -> Result<Json<AdminResponse>, StatusCode> {
let authenticated = authenticate_request(&request_context, &cookies, macaroon).await?;
let authenticated =
authenticate_request(&request_context, "nodes/delete", &cookies, token).await?;
let request = {
let params: Result<DeleteNodeParams, _> = serde_json::from_value(payload);
match params {

22
src/http/macaroon_header.rs → src/http/auth_header.rs

@ -12,10 +12,13 @@ use axum::{
extract::{FromRequest, RequestParts},
};
pub struct MacaroonHeader(pub Option<http::HeaderValue>);
pub struct AuthHeader {
pub macaroon: Option<http::HeaderValue>,
pub token: Option<http::HeaderValue>,
}
#[async_trait]
impl<B> FromRequest<B> for MacaroonHeader
impl<B> FromRequest<B> for AuthHeader
where
B: Send,
{
@ -27,10 +30,19 @@ where
"headers already extracted",
))?;
let mut auth_header = Self {
macaroon: None,
token: None,
};
if let Some(value) = headers.get("macaroon") {
Ok(Self(Some(value.clone())))
} else {
Ok(Self(None))
auth_header.macaroon = Some(value.clone());
}
if let Some(value) = headers.get("token") {
auth_header.token = Some(value.clone());
}
Ok(auth_header)
}
}

2
src/http/mod.rs

@ -8,5 +8,5 @@
// licenses.
pub mod admin;
pub mod macaroon_header;
pub mod auth_header;
pub mod node;

38
src/http/node.rs

@ -9,7 +9,7 @@
use std::sync::Arc;
use crate::http::macaroon_header::MacaroonHeader;
use crate::http::auth_header::AuthHeader;
use crate::services::admin::AdminRequest;
use crate::services::node::{NodeRequest, NodeRequestError, NodeResponse};
use crate::services::{ListChannelsParams, ListPaymentsParams, ListTransactionsParams};
@ -188,7 +188,7 @@ pub fn add_routes(router: Router) -> Router {
pub async fn get_unused_address(
Extension(request_context): Extension<Arc<RequestContext>>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
handle_authenticated_request(
@ -202,7 +202,7 @@ pub async fn get_unused_address(
pub async fn get_wallet_balance(
Extension(request_context): Extension<Arc<RequestContext>>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
handle_authenticated_request(
@ -217,7 +217,7 @@ pub async fn get_wallet_balance(
pub async fn handle_get_payments(
Extension(request_context): Extension<Arc<RequestContext>>,
Query(params): Query<ListPaymentsParams>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = NodeRequest::ListPayments {
@ -231,7 +231,7 @@ pub async fn handle_get_payments(
pub async fn get_channels(
Extension(request_context): Extension<Arc<RequestContext>>,
Query(params): Query<ListChannelsParams>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = NodeRequest::ListChannels {
@ -244,7 +244,7 @@ pub async fn get_channels(
pub async fn get_transactions(
Extension(request_context): Extension<Arc<RequestContext>>,
Query(params): Query<ListTransactionsParams>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = NodeRequest::ListTransactions {
@ -256,7 +256,7 @@ pub async fn get_transactions(
pub async fn get_info(
Extension(request_context): Extension<Arc<RequestContext>>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
handle_authenticated_request(request_context, NodeRequest::NodeInfo {}, macaroon, cookies).await
@ -264,7 +264,7 @@ pub async fn get_info(
pub async fn get_peers(
Extension(request_context): Extension<Arc<RequestContext>>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
handle_authenticated_request(
@ -278,7 +278,7 @@ pub async fn get_peers(
pub async fn stop_node(
Extension(request_context): Extension<Arc<RequestContext>>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
handle_authenticated_request(request_context, NodeRequest::StopNode {}, macaroon, cookies).await
@ -366,7 +366,7 @@ pub async fn handle_authenticated_request(
pub async fn start_node(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -382,7 +382,7 @@ pub async fn start_node(
pub async fn create_invoice(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -398,7 +398,7 @@ pub async fn create_invoice(
pub async fn label_payment(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -414,7 +414,7 @@ pub async fn label_payment(
pub async fn delete_payment(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -430,7 +430,7 @@ pub async fn delete_payment(
pub async fn pay_invoice(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -446,7 +446,7 @@ pub async fn pay_invoice(
pub async fn open_channel(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -462,7 +462,7 @@ pub async fn open_channel(
pub async fn close_channel(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -478,7 +478,7 @@ pub async fn close_channel(
pub async fn keysend(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -494,7 +494,7 @@ pub async fn keysend(
pub async fn connect_peer(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {
@ -510,7 +510,7 @@ pub async fn connect_peer(
pub async fn sign_message(
Extension(request_context): Extension<Arc<RequestContext>>,
Json(payload): Json<Value>,
MacaroonHeader(macaroon): MacaroonHeader,
AuthHeader { macaroon, token: _ }: AuthHeader,
cookies: Cookies,
) -> Result<Json<NodeResponse>, StatusCode> {
let request = {

9
src/main.rs

@ -213,18 +213,11 @@ async fn main() {
}
}
// We use static route matchers ("/" and "/index.html") to serve our home
// page.
async fn index_handler() -> impl IntoResponse {
static_handler("/index.html".parse::<Uri>().unwrap()).await
}
// We use a wildcard matcher ("/static/*file") to match against everything
// within our defined assets directory. This is the directory on our Asset
// struct below, where folder = "examples/public/".
async fn static_handler(uri: Uri) -> impl IntoResponse {
let mut path = uri.path().trim_start_matches('/').to_string();
println!("in static handler with path: {}", path);
if path.starts_with("admin/static/") {
path = path.replace("admin/static/", "static/");
@ -232,8 +225,6 @@ async fn static_handler(uri: Uri) -> impl IntoResponse {
path = String::from("index.html");
}
println!("out static handler with path: {}", path);
StaticFile(path)
}

86
src/services/admin.rs

@ -9,6 +9,7 @@
use super::{PaginationRequest, PaginationResponse};
use crate::chain::manager::SenseiChainManager;
use crate::database::admin::AccessToken;
use crate::database::{
self,
admin::{AdminDatabase, Node, Role, Status},
@ -29,7 +30,7 @@ use std::{collections::hash_map::Entry, fs, sync::Arc};
use tokio::sync::Mutex;
pub enum AdminRequest {
GetStatus {
pubkey: String,
authenticated: bool,
},
CreateAdmin {
username: String,
@ -59,6 +60,18 @@ pub enum AdminRequest {
StopNode {
pubkey: String,
},
CreateToken {
name: String,
expires_at: u64,
scope: String,
single_use: bool,
},
ListTokens {
pagination: PaginationRequest,
},
DeleteToken {
id: u64,
},
}
#[derive(Serialize)]
@ -78,10 +91,12 @@ pub enum AdminResponse {
macaroon: String,
external_id: String,
role: u8,
token: String,
},
StartAdmin {
pubkey: String,
macaroon: String,
token: String,
},
CreateNode {
pubkey: String,
@ -96,6 +111,14 @@ pub enum AdminResponse {
macaroon: String,
},
StopNode {},
CreateToken {
token: AccessToken,
},
ListTokens {
tokens: Vec<AccessToken>,
pagination: PaginationResponse,
},
DeleteToken {},
Error(Error),
}
@ -160,12 +183,11 @@ impl From<macaroon::MacaroonError> for Error {
impl AdminService {
pub async fn call(&self, request: AdminRequest) -> Result<AdminResponse, Error> {
match request {
AdminRequest::GetStatus { pubkey } => {
AdminRequest::GetStatus { authenticated } => {
let mut database = self.database.lock().await;
let admin_node = database.get_admin_node()?;
let created = admin_node.is_some();
let node = database.get_node_by_pubkey(&pubkey)?;
match node {
match admin_node {
Some(node) => {
let directory = self.node_directory.lock().await;
let node_running = directory.contains_key(&node.pubkey);
@ -173,15 +195,15 @@ impl AdminService {
alias: Some(node.alias),
created,
running: node_running,
authenticated: pubkey == node.pubkey,
pubkey: Some(pubkey),
authenticated,
pubkey: Some(node.pubkey),
username: Some(node.username),
role: Some(node.role),
})
}
None => Ok(AdminResponse::GetStatus {
alias: None,
pubkey: Some(pubkey),
pubkey: None,
created,
running: false,
authenticated: false,
@ -199,6 +221,14 @@ impl AdminService {
let (lightning_node, node) = self
.create_node(username, alias, passphrase.clone(), Role::Admin)
.await?;
let access_token = AccessToken::new_admin();
{
let mut database = self.database.lock().await;
database.create_access_token(&access_token).unwrap();
}
let node_info = lightning_node.node_info()?;
let macaroon = lightning_node.macaroon.serialize(macaroon::Format::V2)?;
@ -210,16 +240,17 @@ impl AdminService {
macaroon: hex_utils::hex_str(macaroon.as_slice()),
external_id: node.external_id,
role: node.role,
token: access_token.token,
})
}
AdminRequest::StartAdmin { passphrase } => {
let db_node_result = {
let (db_node, access_token) = {
let mut database = self.database.lock().await;
database.get_admin_node()
let db_node = database.get_admin_node()?;
let access_token = database.get_admin_access_token()?;
(db_node, access_token)
};
let db_node = db_node_result?;
match db_node {
Some(node) => {
let lightning_node = self.start_node(node.clone(), passphrase).await?;
@ -228,6 +259,7 @@ impl AdminService {
Ok(AdminResponse::StartAdmin {
pubkey: node_info.node_pubkey,
macaroon: hex_utils::hex_str(macaroon.as_slice()),
token: access_token.expect("no token in db").token,
})
}
None => Err(Error::Generic(String::from(
@ -307,6 +339,30 @@ impl AdminService {
None => Err(Error::Generic(String::from("node not found"))),
}
}
AdminRequest::CreateToken {
name,
expires_at,
scope,
single_use,
} => {
let access_token = AccessToken::new(name, scope, expires_at, single_use);
let mut database = self.database.lock().await;
database.create_access_token(&access_token)?;
Ok(AdminResponse::CreateToken {
token: access_token,
})
}
AdminRequest::ListTokens { pagination } => {
let (tokens, pagination) = self.list_tokens(pagination).await?;
Ok(AdminResponse::ListTokens { tokens, pagination })
}
AdminRequest::DeleteToken { id } => {
let mut database = self.database.lock().await;
database.delete_access_token(id)?;
Ok(AdminResponse::DeleteToken {})
}
}
}
@ -319,6 +375,14 @@ impl AdminService {
Ok(node)
}
async fn list_tokens(
&self,
pagination: PaginationRequest,
) -> Result<(Vec<AccessToken>, PaginationResponse), crate::error::Error> {
let mut database = self.database.lock().await;
Ok(database.list_access_tokens(pagination)?)
}
async fn list_nodes(
&self,
pagination: PaginationRequest,

8
src/utils.rs

@ -17,10 +17,16 @@ use std::{
pub fn hours_since_epoch() -> Result<u64, SystemTimeError> {
let time_since_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
let hours_since_epoch = time_since_epoch.as_secs() / 3600;
let hours_since_epoch = seconds_since_epoch()? / 3600;
Ok(hours_since_epoch)
}
pub fn seconds_since_epoch() -> Result<u64, SystemTimeError> {
Ok(SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs())
}
pub fn macaroon_from_hex_str(hex_str: &str) -> Result<Macaroon, Error> {
let macaroon_byte_vec = hex_utils::to_vec(hex_str).unwrap();
Macaroon::deserialize(macaroon_byte_vec.as_slice()).map_err(Error::Macaroon)

Loading…
Cancel
Save