Skip to content

Commit

Permalink
API keys listing endpoint
Browse files Browse the repository at this point in the history
Signed-off-by: Filippo Costa <[email protected]>
  • Loading branch information
neysofu committed Jun 2, 2024
1 parent 05a910d commit c74152c
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 89 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,3 @@ rustc-ice*

### Project specific
ops/compose/data/*
/frontend/dist
frontend/graphql/api_schema.graphql
7 changes: 7 additions & 0 deletions crates/autogen_graphql_schema/api_schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ enum ApiKeyPermissionLevel {
ADMIN
}

type ApiKeyPublicMetadata {
publicPrefix: String!
notes: String
permissionLevel: ApiKeyPermissionLevel!
}

"""
Metadata that was collected during a bisection run.
"""
Expand Down Expand Up @@ -437,6 +443,7 @@ type QueryRoot {
subgraph deployment.
"""
liveProofsOfIndexing(filter: PoisQuery!): [ProofOfIndexing!]!
apiKeys: [ApiKeyPublicMetadata!]!
poiAgreementRatios(indexerAddress: HexString!): [PoiAgreementRatio!]!
divergenceInvestigationReport(
"""
Expand Down
43 changes: 43 additions & 0 deletions crates/common_types/src/api_key_permission_level.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use diesel::deserialize::{FromSql, FromSqlRow};
use diesel::expression::AsExpression;
use diesel::pg::{Pg, PgValue};
use diesel::serialize::ToSql;
use diesel::sql_types;

#[derive(
Debug,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
AsExpression,
FromSqlRow,
async_graphql::Enum,
)]
#[diesel(sql_type = sql_types::Integer)]
#[repr(i32)]
pub enum ApiKeyPermissionLevel {
Admin,
}

impl ToSql<sql_types::Integer, Pg> for ApiKeyPermissionLevel {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, Pg>,
) -> diesel::serialize::Result {
match self {
ApiKeyPermissionLevel::Admin => <i32 as ToSql<sql_types::Integer, Pg>>::to_sql(&1, out),
}
}
}

impl FromSql<sql_types::Integer, Pg> for ApiKeyPermissionLevel {
fn from_sql(bytes: PgValue<'_>) -> diesel::deserialize::Result<Self> {
match i32::from_sql(bytes)? {
1 => Ok(ApiKeyPermissionLevel::Admin),
_ => Err(anyhow::anyhow!("invalid permission level").into()),
}
}
}
17 changes: 2 additions & 15 deletions crates/common_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
//! A few of these are shared with database models as well. Should we keep them
//! separate? It would be cleaner, but at the cost of some code duplication.
mod api_key_permission_level;
mod hex_string;
pub mod inputs;
mod ipfs_cid;

pub use api_key_permission_level::ApiKeyPermissionLevel;
use async_graphql::*;
use chrono::NaiveDateTime;
pub use divergence_investigation::*;
Expand Down Expand Up @@ -170,18 +172,3 @@ pub struct PoiCrossCheckReport {
proof_of_indexing2: PoiBytes,
diverging_block: Option<DivergingBlock>,
}

#[derive(
Debug,
Copy,
Clone,
PartialEq,
Eq,
async_graphql::Enum,
// strum is used for (de)serialization in the database.
strum::Display,
strum::EnumString,
)]
pub enum ApiKeyPermissionLevel {
Admin,
}
34 changes: 32 additions & 2 deletions crates/graphix_lib/src/graphql_api/mutation_root.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::str::FromStr;

use async_graphql::{Context, Object, Result};
use graphix_common_types::*;
use graphix_store::models::{DivergenceInvestigationRequest, NewlyCreatedApiKey};
Expand Down Expand Up @@ -75,6 +73,10 @@ impl MutationRoot {
)]
notes: Option<String>,
) -> Result<NewlyCreatedApiKey> {
// In order to create an API key with a certain permission level, you
// need to have that permission level yourself.
require_permission_level(ctx, permission_level).await?;

let ctx_data = ctx_data(ctx);

let api_key = ctx_data
Expand Down Expand Up @@ -103,6 +105,8 @@ impl MutationRoot {
notes: Option<String>,
permission_level: ApiKeyPermissionLevel,
) -> Result<bool> {
require_permission_level(ctx, permission_level).await?;

let ctx_data = ctx_data(ctx);

ctx_data
Expand Down Expand Up @@ -139,3 +143,29 @@ impl MutationRoot {
Ok(network)
}
}

async fn require_permission_level(
ctx: &Context<'_>,
required_permission_level: ApiKeyPermissionLevel,
) -> Result<()> {
let ctx_data = ctx_data(ctx);
let api_key = ctx_data
.api_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No API key provided"))?;

let Some(actual_permission_level) = ctx_data.store.permission_level(&api_key).await? else {
return Err(anyhow::anyhow!("No permission level for API key").into());
};

if actual_permission_level < required_permission_level {
return Err(anyhow::anyhow!(
"Insufficient permission level for API key: expected {:?}, got {:?}",
required_permission_level,
actual_permission_level
)
.into());
}

Ok(())
}
8 changes: 8 additions & 0 deletions crates/graphix_lib/src/graphql_api/query_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::Context as _;
use async_graphql::{Context, Object, Result};
use futures::future::try_join_all;
use graphix_common_types::*;
use graphix_store::models::ApiKeyPublicMetadata;
use uuid::Uuid;

use super::{api_types, ctx_data};
Expand Down Expand Up @@ -129,6 +130,13 @@ impl QueryRoot {
Ok(pois.into_iter().map(Into::into).collect())
}

async fn api_keys(&self, ctx: &Context<'_>) -> Result<Vec<ApiKeyPublicMetadata>> {
let ctx_data = ctx_data(ctx);
let api_keys = ctx_data.store.api_keys().await?;

Ok(api_keys)
}

async fn poi_agreement_ratios(
&self,
ctx: &Context<'_>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,5 @@ CREATE TABLE graphix_api_tokens (
public_prefix TEXT PRIMARY KEY,
sha256_api_key_hash BYTEA NOT NULL UNIQUE,
notes TEXT,
-- We shouldn't really store permission levels as `TEXT` but... it works.
permission_level TEXT NOT NULL
permission_level INTEGER NOT NULL
);
66 changes: 58 additions & 8 deletions crates/store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
use graphix_common_types::{inputs, ApiKeyPermissionLevel, IndexerAddress, IpfsCid, PoiBytes};
use models::{
ApiKey, FailedQueryRow, NewIndexerNetworkSubgraphMetadata, NewlyCreatedApiKey, SgDeployment,
ApiKey, ApiKeyDbRow, ApiKeyPublicMetadata, FailedQueryRow, NewIndexerNetworkSubgraphMetadata,
NewlyCreatedApiKey, SgDeployment,
};
use uuid::Uuid;
pub mod models;
Expand Down Expand Up @@ -57,6 +58,11 @@ impl Store {

store.run_migrations().await?;

if store.api_keys().await?.is_empty() {
info!("No API keys found in database, creating master API key");
store.create_master_api_key().await?;
}

Ok(store)
}

Expand All @@ -73,6 +79,24 @@ impl Store {
Ok(())
}

async fn create_master_api_key(&self) -> anyhow::Result<()> {
let api_key = self
.create_api_key(None, ApiKeyPermissionLevel::Admin)
.await?;

let description = format!("Master API key created during database initialization. Use it to create a new private API key and then delete it for security reasons. `{}`", api_key.api_key.to_string());
self.modify_api_key(
&api_key.api_key,
Some(&description),
ApiKeyPermissionLevel::Admin,
)
.await?;

info!(api_key = ?api_key.api_key, "Created master API key");

Ok(())
}

pub async fn conn(&self) -> anyhow::Result<Object<AsyncPgConnection>> {
Ok(self.pool.get().await?)
}
Expand Down Expand Up @@ -421,14 +445,15 @@ impl Store {
use schema::graphix_api_tokens;

let api_key = ApiKey::generate();
let stored_api_key = ApiKeyDbRow {
public_prefix: api_key.public_part_as_string(),
sha256_api_key_hash: api_key.hash(),
notes: notes.map(|s| s.to_string()),
permission_level,
};

diesel::insert_into(graphix_api_tokens::table)
.values((
graphix_api_tokens::public_prefix.eq(api_key.public_part_as_string()),
graphix_api_tokens::sha256_api_key_hash.eq(api_key.hash()),
graphix_api_tokens::notes.eq(notes),
graphix_api_tokens::permission_level.eq(permission_level.to_string()),
))
.values(&[stored_api_key])
.execute(&mut self.conn().await?)
.await?;

Expand All @@ -453,7 +478,7 @@ impl Store {
.filter(graphix_api_tokens::sha256_api_key_hash.eq(api_key.hash()))
.set((
graphix_api_tokens::notes.eq(notes),
graphix_api_tokens::permission_level.eq(permission_level.to_string()),
graphix_api_tokens::permission_level.eq(permission_level),
))
.execute(&mut self.conn().await?)
.await?;
Expand All @@ -474,6 +499,31 @@ impl Store {
Ok(())
}

pub async fn api_keys(&self) -> anyhow::Result<Vec<ApiKeyPublicMetadata>> {
use schema::graphix_api_tokens;

Ok(graphix_api_tokens::table
.load::<ApiKeyDbRow>(&mut self.conn().await?)
.await?
.into_iter()
.map(ApiKeyPublicMetadata::from)
.collect())
}

pub async fn permission_level(
&self,
api_key: &ApiKey,
) -> anyhow::Result<Option<ApiKeyPermissionLevel>> {
use schema::graphix_api_tokens;

Ok(graphix_api_tokens::table
.select(graphix_api_tokens::permission_level)
.filter(graphix_api_tokens::sha256_api_key_hash.eq(api_key.hash()))
.get_result(&mut self.conn().await?)
.await
.optional()?)
}

pub async fn write_graph_node_versions(
&self,
versions: HashMap<
Expand Down
32 changes: 29 additions & 3 deletions crates/store/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,32 @@ impl IndexerId for Indexer {
}
}

#[derive(Debug, Clone, Insertable, Queryable, Selectable)]
#[diesel(table_name = graphix_api_tokens)]
pub struct ApiKeyDbRow {
pub public_prefix: String,
pub sha256_api_key_hash: Vec<u8>,
pub notes: Option<String>,
pub permission_level: ApiKeyPermissionLevel,
}

#[derive(Debug, Clone, SimpleObject)]
pub struct ApiKeyPublicMetadata {
pub public_prefix: String,
pub notes: Option<String>,
pub permission_level: ApiKeyPermissionLevel,
}

impl From<ApiKeyDbRow> for ApiKeyPublicMetadata {
fn from(sak: ApiKeyDbRow) -> Self {
Self {
public_prefix: sak.public_prefix,
notes: sak.notes,
permission_level: sak.permission_level,
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiKey {
public_part: Uuid,
Expand All @@ -144,7 +170,7 @@ impl ApiKey {
}

pub fn public_part_as_string(&self) -> String {
self.public_part.to_string()
self.public_part.as_simple().to_string()
}

pub fn hash(&self) -> Vec<u8> {
Expand All @@ -159,7 +185,7 @@ impl std::str::FromStr for ApiKey {
let parts: Vec<&str> = s.split('-').collect();
let parts: [&str; 3] = parts.try_into().map_err(|_| "invalid api key format")?;

if parts[0] != "graphix_api_key" {
if parts[0] != "graphix" {
return Err("invalid api key format".to_string());
}

Expand All @@ -177,7 +203,7 @@ impl std::fmt::Display for ApiKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"graphix_api_key-{}-{}",
"graphix-{}-{}",
self.public_part.as_simple(),
self.private_part.as_simple()
)
Expand Down
2 changes: 1 addition & 1 deletion crates/store/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ diesel::table! {
public_prefix -> Text,
sha256_api_key_hash -> Bytea,
notes -> Nullable<Text>,
permission_level -> Text,
permission_level -> Int4,
}
}

Expand Down
4 changes: 1 addition & 3 deletions ops/compose/dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3"

services:
grafana:
image: grafana/grafana-oss
image: grafana/grafana-oss:9.3.16
restart: unless-stopped
# https://community.grafana.com/t/new-docker-install-with-persistent-storage-permission-problem/10896/16
user: ":"
Expand All @@ -12,8 +12,6 @@ services:
ports:
- "3000:3000"
environment:
# Plugins:
# - https://github.com/fifemon/graphql-datasource for GraphQL data sources.
- GF_INSTALL_PLUGINS=fifemon-graphql-datasource,yesoreyeram-infinity-datasource
volumes:
- ./grafana/config/:/etc/grafana/
Expand Down
Loading

0 comments on commit c74152c

Please sign in to comment.