From 70d569085bcdbdd124f507b0458f006cea3e5921 Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Wed, 15 Nov 2023 12:43:34 +0100 Subject: [PATCH 1/8] feat: add read_only field to api-keys --- ...23904347ad20dc726e79c8fcaacc6a351008.json} | 10 ++++++++-- ...0dec28654d37c9812ddd7a6245cf26cf64ec.json} | 7 ++++--- ...89b17bde77e7bfef55430bbe2ba75b1dfcbd.json} | 10 ++++++++-- ...ee52d743b27cebe180e712ea808cef3798da.json} | 10 ++++++++-- ...501_add-read-only-to-identity-api-keys.sql | 2 ++ core/api-keys/src/app/mod.rs | 11 +++++++--- core/api-keys/src/graphql/schema.rs | 9 ++++++++- core/api-keys/src/identity/mod.rs | 20 +++++++++++++------ core/api-keys/src/server/mod.rs | 5 +++-- core/api-keys/subgraph/schema.graphql | 1 + .../dev/apollo-federation/supergraph.graphql | 1 + .../apollo-federation/supergraph.graphql | 1 + 12 files changed, 66 insertions(+), 21 deletions(-) rename core/api-keys/.sqlx/{query-f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33.json => query-199062a005df2f2ab23834721f4823904347ad20dc726e79c8fcaacc6a351008.json} (68%) rename core/api-keys/.sqlx/{query-799aef2d29106de6822e1f66ee9f7a71d3c844edd6aef491af4621cf73e11234.json => query-d0f9efb8a6ab2c7b36cee50f664b0dec28654d37c9812ddd7a6245cf26cf64ec.json} (66%) rename core/api-keys/.sqlx/{query-de6ccead87f1624a72b18a0c7f715f21c1d3caea71cf9d86ddee3180d3673995.json => query-f53f91f541e68cc541117f51440989b17bde77e7bfef55430bbe2ba75b1dfcbd.json} (82%) rename core/api-keys/.sqlx/{query-99bc6db0b354e8a7ef70fe64d2bd7dd5da9d3d4f6ffa47296e8678fa902cf5e4.json => query-fa550a28f90f8cdbacf62f56e642ee52d743b27cebe180e712ea808cef3798da.json} (72%) create mode 100644 core/api-keys/migrations/20231115113501_add-read-only-to-identity-api-keys.sql diff --git a/core/api-keys/.sqlx/query-f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33.json b/core/api-keys/.sqlx/query-199062a005df2f2ab23834721f4823904347ad20dc726e79c8fcaacc6a351008.json similarity index 68% rename from core/api-keys/.sqlx/query-f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33.json rename to core/api-keys/.sqlx/query-199062a005df2f2ab23834721f4823904347ad20dc726e79c8fcaacc6a351008.json index 77bb666dd0..c0bca04767 100644 --- a/core/api-keys/.sqlx/query-f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33.json +++ b/core/api-keys/.sqlx/query-199062a005df2f2ab23834721f4823904347ad20dc726e79c8fcaacc6a351008.json @@ -1,12 +1,17 @@ { "db_name": "PostgreSQL", - "query": "WITH updated_key AS (\n UPDATE identity_api_keys k\n SET last_used_at = NOW()\n FROM identities i\n WHERE k.identity_id = i.id\n AND k.revoked = false\n AND k.encrypted_key = crypt($1, k.encrypted_key)\n AND k.expires_at > NOW()\n RETURNING k.id, i.subject_id\n )\n SELECT subject_id FROM updated_key", + "query": "WITH updated_key AS (\n UPDATE identity_api_keys k\n SET last_used_at = NOW()\n FROM identities i\n WHERE k.identity_id = i.id\n AND k.revoked = false\n AND k.encrypted_key = crypt($1, k.encrypted_key)\n AND k.expires_at > NOW()\n RETURNING k.id, i.subject_id, k.read_only\n )\n SELECT subject_id, read_only FROM updated_key", "describe": { "columns": [ { "ordinal": 0, "name": "subject_id", "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "read_only", + "type_info": "Bool" } ], "parameters": { @@ -15,8 +20,9 @@ ] }, "nullable": [ + false, false ] }, - "hash": "f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33" + "hash": "199062a005df2f2ab23834721f4823904347ad20dc726e79c8fcaacc6a351008" } diff --git a/core/api-keys/.sqlx/query-799aef2d29106de6822e1f66ee9f7a71d3c844edd6aef491af4621cf73e11234.json b/core/api-keys/.sqlx/query-d0f9efb8a6ab2c7b36cee50f664b0dec28654d37c9812ddd7a6245cf26cf64ec.json similarity index 66% rename from core/api-keys/.sqlx/query-799aef2d29106de6822e1f66ee9f7a71d3c844edd6aef491af4621cf73e11234.json rename to core/api-keys/.sqlx/query-d0f9efb8a6ab2c7b36cee50f664b0dec28654d37c9812ddd7a6245cf26cf64ec.json index 12b3431da4..955fc96b79 100644 --- a/core/api-keys/.sqlx/query-799aef2d29106de6822e1f66ee9f7a71d3c844edd6aef491af4621cf73e11234.json +++ b/core/api-keys/.sqlx/query-d0f9efb8a6ab2c7b36cee50f664b0dec28654d37c9812ddd7a6245cf26cf64ec.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO identity_api_keys (encrypted_key, identity_id, name, expires_at)\n VALUES (crypt($1, gen_salt('bf')), $2, $3, $4) RETURNING id, created_at", + "query": "INSERT INTO identity_api_keys (encrypted_key, identity_id, name, expires_at, read_only)\n VALUES (crypt($1, gen_salt('bf')), $2, $3, $4, $5) RETURNING id, created_at", "describe": { "columns": [ { @@ -19,7 +19,8 @@ "Text", "Uuid", "Varchar", - "Timestamptz" + "Timestamptz", + "Bool" ] }, "nullable": [ @@ -27,5 +28,5 @@ false ] }, - "hash": "799aef2d29106de6822e1f66ee9f7a71d3c844edd6aef491af4621cf73e11234" + "hash": "d0f9efb8a6ab2c7b36cee50f664b0dec28654d37c9812ddd7a6245cf26cf64ec" } diff --git a/core/api-keys/.sqlx/query-de6ccead87f1624a72b18a0c7f715f21c1d3caea71cf9d86ddee3180d3673995.json b/core/api-keys/.sqlx/query-f53f91f541e68cc541117f51440989b17bde77e7bfef55430bbe2ba75b1dfcbd.json similarity index 82% rename from core/api-keys/.sqlx/query-de6ccead87f1624a72b18a0c7f715f21c1d3caea71cf9d86ddee3180d3673995.json rename to core/api-keys/.sqlx/query-f53f91f541e68cc541117f51440989b17bde77e7bfef55430bbe2ba75b1dfcbd.json index df2348c65d..a19e7fc64d 100644 --- a/core/api-keys/.sqlx/query-de6ccead87f1624a72b18a0c7f715f21c1d3caea71cf9d86ddee3180d3673995.json +++ b/core/api-keys/.sqlx/query-f53f91f541e68cc541117f51440989b17bde77e7bfef55430bbe2ba75b1dfcbd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE identity_api_keys k\n SET revoked = true,\n revoked_at = NOW()\n FROM identities i\n WHERE k.identity_id = i.id\n AND i.subject_id = $1\n AND k.id = $2\n RETURNING\n k.name,\n k.identity_id,\n k.created_at,\n k.expires_at,\n k.revoked,\n expires_at < NOW() AS \"expired!\",\n k.last_used_at\n ", + "query": "UPDATE identity_api_keys k\n SET revoked = true,\n revoked_at = NOW()\n FROM identities i\n WHERE k.identity_id = i.id\n AND i.subject_id = $1\n AND k.id = $2\n RETURNING\n k.name,\n k.identity_id,\n k.created_at,\n k.expires_at,\n k.revoked,\n expires_at < NOW() AS \"expired!\",\n k.read_only,\n k.last_used_at\n ", "describe": { "columns": [ { @@ -35,6 +35,11 @@ }, { "ordinal": 6, + "name": "read_only", + "type_info": "Bool" + }, + { + "ordinal": 7, "name": "last_used_at", "type_info": "Timestamptz" } @@ -52,8 +57,9 @@ false, false, null, + false, true ] }, - "hash": "de6ccead87f1624a72b18a0c7f715f21c1d3caea71cf9d86ddee3180d3673995" + "hash": "f53f91f541e68cc541117f51440989b17bde77e7bfef55430bbe2ba75b1dfcbd" } diff --git a/core/api-keys/.sqlx/query-99bc6db0b354e8a7ef70fe64d2bd7dd5da9d3d4f6ffa47296e8678fa902cf5e4.json b/core/api-keys/.sqlx/query-fa550a28f90f8cdbacf62f56e642ee52d743b27cebe180e712ea808cef3798da.json similarity index 72% rename from core/api-keys/.sqlx/query-99bc6db0b354e8a7ef70fe64d2bd7dd5da9d3d4f6ffa47296e8678fa902cf5e4.json rename to core/api-keys/.sqlx/query-fa550a28f90f8cdbacf62f56e642ee52d743b27cebe180e712ea808cef3798da.json index 55a2d9363e..1eef359d87 100644 --- a/core/api-keys/.sqlx/query-99bc6db0b354e8a7ef70fe64d2bd7dd5da9d3d4f6ffa47296e8678fa902cf5e4.json +++ b/core/api-keys/.sqlx/query-fa550a28f90f8cdbacf62f56e642ee52d743b27cebe180e712ea808cef3798da.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n i.id AS identity_id,\n a.id AS api_key_id,\n a.name,\n a.created_at,\n a.expires_at,\n revoked,\n expires_at < NOW() AS \"expired!\",\n last_used_at\n FROM\n identities i\n JOIN\n identity_api_keys a\n ON i.id = a.identity_id\n WHERE\n i.subject_id = $1\n ", + "query": "\n SELECT\n i.id AS identity_id,\n a.id AS api_key_id,\n a.name,\n a.created_at,\n a.expires_at,\n revoked,\n expires_at < NOW() AS \"expired!\",\n read_only,\n last_used_at\n FROM\n identities i\n JOIN\n identity_api_keys a\n ON i.id = a.identity_id\n WHERE\n i.subject_id = $1\n ", "describe": { "columns": [ { @@ -40,6 +40,11 @@ }, { "ordinal": 7, + "name": "read_only", + "type_info": "Bool" + }, + { + "ordinal": 8, "name": "last_used_at", "type_info": "Timestamptz" } @@ -57,8 +62,9 @@ false, false, null, + false, true ] }, - "hash": "99bc6db0b354e8a7ef70fe64d2bd7dd5da9d3d4f6ffa47296e8678fa902cf5e4" + "hash": "fa550a28f90f8cdbacf62f56e642ee52d743b27cebe180e712ea808cef3798da" } diff --git a/core/api-keys/migrations/20231115113501_add-read-only-to-identity-api-keys.sql b/core/api-keys/migrations/20231115113501_add-read-only-to-identity-api-keys.sql new file mode 100644 index 0000000000..20ae59ddc8 --- /dev/null +++ b/core/api-keys/migrations/20231115113501_add-read-only-to-identity-api-keys.sql @@ -0,0 +1,2 @@ +ALTER TABLE identity_api_keys +ADD COLUMN read_only BOOL NOT NULL DEFAULT false; diff --git a/core/api-keys/src/app/mod.rs b/core/api-keys/src/app/mod.rs index 1f2f4d000f..bd832819e1 100644 --- a/core/api-keys/src/app/mod.rs +++ b/core/api-keys/src/app/mod.rs @@ -31,7 +31,7 @@ impl ApiKeysApp { pub async fn lookup_authenticated_subject( &self, key: &str, - ) -> Result { + ) -> Result<(String, bool), ApplicationError> { Ok(self.identities.find_subject_by_key(&key).await?) } @@ -40,16 +40,21 @@ impl ApiKeysApp { &self, subject_id: &str, name: String, + expire_in_days: Option, + read_only: bool, ) -> Result<(IdentityApiKey, ApiKeySecret), ApplicationError> { let mut tx = self.pool.begin().await?; let id = self .identities .find_or_create_identity_for_subject_in_tx(&mut tx, subject_id) .await?; - let expiry = chrono::Utc::now() + self.config.default_expiry(); + let expiry = chrono::Utc::now() + + expire_in_days + .map(|days| std::time::Duration::from_secs(days as u64 * 24 * 60 * 60)) + .unwrap_or_else(|| self.config.default_expiry()); let key = self .identities - .create_key_for_identity_in_tx(&mut tx, id, name, expiry) + .create_key_for_identity_in_tx(&mut tx, id, name, expiry, read_only) .await?; tx.commit().await?; Ok(key) diff --git a/core/api-keys/src/graphql/schema.rs b/core/api-keys/src/graphql/schema.rs index 8439e64340..0667fe478f 100644 --- a/core/api-keys/src/graphql/schema.rs +++ b/core/api-keys/src/graphql/schema.rs @@ -102,6 +102,8 @@ pub struct Mutation; struct ApiKeyCreateInput { name: String, expire_in_days: Option, + #[graphql(default)] + read_only: bool, } #[derive(InputObject)] @@ -119,7 +121,12 @@ impl Mutation { let app = ctx.data_unchecked::(); let subject = ctx.data::()?; let key = app - .create_api_key_for_subject(&subject.id, input.name) + .create_api_key_for_subject( + &subject.id, + input.name, + input.expire_in_days, + input.read_only, + ) .await?; Ok(ApiKeyCreatePayload::from(key)) } diff --git a/core/api-keys/src/identity/mod.rs b/core/api-keys/src/identity/mod.rs index 2eae045813..3e8f8d88bf 100644 --- a/core/api-keys/src/identity/mod.rs +++ b/core/api-keys/src/identity/mod.rs @@ -20,6 +20,7 @@ pub struct IdentityApiKey { pub last_used_at: Option>, pub revoked: bool, pub expired: bool, + pub read_only: bool, } pub struct ApiKeySecret(String); @@ -62,15 +63,17 @@ impl Identities { identity_id: IdentityId, name: String, expires_at: chrono::DateTime, + read_only: bool, ) -> Result<(IdentityApiKey, ApiKeySecret), IdentityError> { let code = Alphanumeric.sample_string(&mut rand::thread_rng(), 64); let record = sqlx::query!( - r#"INSERT INTO identity_api_keys (encrypted_key, identity_id, name, expires_at) - VALUES (crypt($1, gen_salt('bf')), $2, $3, $4) RETURNING id, created_at"#, + r#"INSERT INTO identity_api_keys (encrypted_key, identity_id, name, expires_at, read_only) + VALUES (crypt($1, gen_salt('bf')), $2, $3, $4, $5) RETURNING id, created_at"#, code, identity_id as IdentityId, name, expires_at, + read_only, ) .fetch_one(&mut **tx) .await?; @@ -86,12 +89,13 @@ impl Identities { revoked: false, expired: false, last_used_at: None, + read_only, }, ApiKeySecret(key), )) } - pub async fn find_subject_by_key(&self, key: &str) -> Result { + pub async fn find_subject_by_key(&self, key: &str) -> Result<(String, bool), IdentityError> { let code = match key.strip_prefix(&*self.key_prefix) { None => return Err(IdentityError::MismatchedPrefix), Some(code) => code, @@ -106,16 +110,16 @@ impl Identities { AND k.revoked = false AND k.encrypted_key = crypt($1, k.encrypted_key) AND k.expires_at > NOW() - RETURNING k.id, i.subject_id + RETURNING k.id, i.subject_id, k.read_only ) - SELECT subject_id FROM updated_key"#, + SELECT subject_id, read_only FROM updated_key"#, code ) .fetch_optional(&self.pool) .await?; if let Some(record) = record { - Ok(record.subject_id) + Ok((record.subject_id, record.read_only)) } else { Err(IdentityError::NoActiveKeyFound) } @@ -135,6 +139,7 @@ impl Identities { a.expires_at, revoked, expires_at < NOW() AS "expired!", + read_only, last_used_at FROM identities i @@ -160,6 +165,7 @@ impl Identities { revoked: record.revoked, expired: record.expired, last_used_at: record.last_used_at, + read_only: record.read_only, }) .collect(); @@ -186,6 +192,7 @@ impl Identities { k.expires_at, k.revoked, expires_at < NOW() AS "expired!", + k.read_only, k.last_used_at "#, subject_id, @@ -204,6 +211,7 @@ impl Identities { revoked: record.revoked, expired: record.expired, last_used_at: record.last_used_at, + read_only: record.read_only, }), None => Err(IdentityError::KeyNotFoundForRevoke), } diff --git a/core/api-keys/src/server/mod.rs b/core/api-keys/src/server/mod.rs index a83a0b9087..e33ac41a61 100644 --- a/core/api-keys/src/server/mod.rs +++ b/core/api-keys/src/server/mod.rs @@ -57,6 +57,7 @@ pub async fn run_server(config: ServerConfig, api_keys_app: ApiKeysApp) -> anyho #[derive(Debug, Serialize)] struct CheckResponse { sub: String, + read_only: bool, } async fn check_handler( @@ -64,8 +65,8 @@ async fn check_handler( headers: HeaderMap, ) -> Result, ApplicationError> { let key = headers.get(header).ok_or(ApplicationError::MissingApiKey)?; - let sub = app.lookup_authenticated_subject(key.to_str()?).await?; - Ok(Json(CheckResponse { sub })) + let (sub, read_only) = app.lookup_authenticated_subject(key.to_str()?).await?; + Ok(Json(CheckResponse { sub, read_only })) } pub async fn graphql_handler( diff --git a/core/api-keys/subgraph/schema.graphql b/core/api-keys/subgraph/schema.graphql index 75687e9dd7..37349e47ea 100644 --- a/core/api-keys/subgraph/schema.graphql +++ b/core/api-keys/subgraph/schema.graphql @@ -11,6 +11,7 @@ type ApiKey { input ApiKeyCreateInput { name: String! expireInDays: Int + readOnly: Boolean! = false } type ApiKeyCreatePayload { diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index 861c96d64b..aaf0bebd00 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -187,6 +187,7 @@ input ApiKeyCreateInput { name: String! expireInDays: Int + readOnly: Boolean! = false } type ApiKeyCreatePayload diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index 861c96d64b..aaf0bebd00 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -187,6 +187,7 @@ input ApiKeyCreateInput { name: String! expireInDays: Int + readOnly: Boolean! = false } type ApiKeyCreatePayload From 5f731122aac6cc61750ab6f43fe4e51c87a02bf2 Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Wed, 15 Nov 2023 14:35:10 +0100 Subject: [PATCH 2/8] chore: add readOnly to bats test --- bats/core/api-keys/api-keys.bats | 26 +++++++++++++++++++ bats/gql/api-key-create.gql | 1 + bats/gql/api-keys.gql | 1 + core/api-keys/src/graphql/convert.rs | 1 + core/api-keys/src/graphql/schema.rs | 1 + core/api-keys/subgraph/schema.graphql | 1 + .../dev/apollo-federation/supergraph.graphql | 1 + .../apollo-federation/supergraph.graphql | 1 + 8 files changed, 33 insertions(+) diff --git a/bats/core/api-keys/api-keys.bats b/bats/core/api-keys/api-keys.bats index 01b25bb5e9..a94869861c 100644 --- a/bats/core/api-keys/api-keys.bats +++ b/bats/core/api-keys/api-keys.bats @@ -33,6 +33,10 @@ new_key_name() { name=$(echo "$key" | jq -r '.name') [[ "${name}" = "${key_name}" ]] || exit 1 + + readOnly=$(echo "$key" | jq -r '.readOnly') + [[ "${readOnly}" = "false" ]] || exit 1 + key_id=$(echo "$key" | jq -r '.id') cache_value "api-key-id" "$key_id" @@ -67,3 +71,25 @@ new_key_name() { error="$(graphql_output '.error.code')" [[ "${error}" = "401" ]] || exit 1 } + +@test "api-keys: can create read-only" { + key_name="$(new_key_name)" + + variables="{\"input\":{\"name\":\"${key_name}\",\"readOnly\": true}}" + + exec_graphql 'alice' 'api-key-create' "$variables" + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + cache_value "api-key-secret" "$secret" + + readOnly=$(echo "$key" | jq -r '.readOnly') + [[ "${readOnly}" = "true" ]] || exit 1 + + key_id=$(echo "$key" | jq -r '.id') + cache_value "api-key-id" "$key_id" + + exec_graphql 'api-key-secret' 'api-keys' + + name="$(graphql_output '.data.me.apiKeys[-1].name')" + [[ "${name}" = "${key_name}" ]] || exit 1 +} diff --git a/bats/gql/api-key-create.gql b/bats/gql/api-key-create.gql index 432fc5e635..aaeb1e7cd0 100644 --- a/bats/gql/api-key-create.gql +++ b/bats/gql/api-key-create.gql @@ -5,6 +5,7 @@ mutation apiKeyCreate($input: ApiKeyCreateInput!) { name createdAt expiresAt + readOnly } apiKeySecret } diff --git a/bats/gql/api-keys.gql b/bats/gql/api-keys.gql index a436709da5..2537653784 100644 --- a/bats/gql/api-keys.gql +++ b/bats/gql/api-keys.gql @@ -9,6 +9,7 @@ query apiKeys { revoked createdAt expiresAt + readOnly } } } diff --git a/core/api-keys/src/graphql/convert.rs b/core/api-keys/src/graphql/convert.rs index c59d4b5243..68b10bb115 100644 --- a/core/api-keys/src/graphql/convert.rs +++ b/core/api-keys/src/graphql/convert.rs @@ -23,6 +23,7 @@ impl From for ApiKey { last_used_at: key.last_used_at.map(Timestamp::from), created_at: Timestamp::from(key.created_at), expires_at: Timestamp::from(key.expires_at), + read_only: key.read_only, } } } diff --git a/core/api-keys/src/graphql/schema.rs b/core/api-keys/src/graphql/schema.rs index 0667fe478f..2da851da51 100644 --- a/core/api-keys/src/graphql/schema.rs +++ b/core/api-keys/src/graphql/schema.rs @@ -62,6 +62,7 @@ pub(super) struct ApiKey { pub expired: bool, pub last_used_at: Option, pub expires_at: Timestamp, + pub read_only: bool, } #[derive(SimpleObject)] diff --git a/core/api-keys/subgraph/schema.graphql b/core/api-keys/subgraph/schema.graphql index 37349e47ea..9ede365a78 100644 --- a/core/api-keys/subgraph/schema.graphql +++ b/core/api-keys/subgraph/schema.graphql @@ -6,6 +6,7 @@ type ApiKey { expired: Boolean! lastUsedAt: Timestamp expiresAt: Timestamp! + readOnly: Boolean! } input ApiKeyCreateInput { diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index aaf0bebd00..b0c019e4eb 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -180,6 +180,7 @@ type ApiKey expired: Boolean! lastUsedAt: Timestamp expiresAt: Timestamp! + readOnly: Boolean! } input ApiKeyCreateInput diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index aaf0bebd00..b0c019e4eb 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -180,6 +180,7 @@ type ApiKey expired: Boolean! lastUsedAt: Timestamp expiresAt: Timestamp! + readOnly: Boolean! } input ApiKeyCreateInput From 488ac08ffd6941f09d9a29ba8ca13aeff3aaa9e0 Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Wed, 15 Nov 2023 16:12:39 +0100 Subject: [PATCH 3/8] chore: restrict mutations in api-keys --- bats/core/api-keys/api-keys.bats | 8 ++++++++ bats/gql/update-display-currency.gql | 7 +++++++ core/api-keys/src/graphql/schema.rs | 4 ++++ core/api-keys/src/server/mod.rs | 7 ++++++- dev/config/ory/oathkeeper_rules.yaml | 2 +- 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 bats/gql/update-display-currency.gql diff --git a/bats/core/api-keys/api-keys.bats b/bats/core/api-keys/api-keys.bats index a94869861c..3aef409050 100644 --- a/bats/core/api-keys/api-keys.bats +++ b/bats/core/api-keys/api-keys.bats @@ -93,3 +93,11 @@ new_key_name() { name="$(graphql_output '.data.me.apiKeys[-1].name')" [[ "${name}" = "${key_name}" ]] || exit 1 } + +@test "api-keys: read-only key cannot mutate" { + key_name="$(new_key_name)" + variables="{\"input\":{\"name\":\"${key_name}\"}}" + exec_graphql 'api-key-secret' 'api-key-create' "$variables" + errors="$(graphql_output '.errors | length')" + [[ "${errors}" = "1" ]] || exit 1 +} diff --git a/bats/gql/update-display-currency.gql b/bats/gql/update-display-currency.gql new file mode 100644 index 0000000000..a813ceef1d --- /dev/null +++ b/bats/gql/update-display-currency.gql @@ -0,0 +1,7 @@ +mutation displayCurrencyUpdate($input: AccountUpdateDisplayCurrencyInput!) { + accountUpdateDisplayCurrency(input: $input) { + account { + displayCurrency + } + } +} diff --git a/core/api-keys/src/graphql/schema.rs b/core/api-keys/src/graphql/schema.rs index 2da851da51..b7259b3ef2 100644 --- a/core/api-keys/src/graphql/schema.rs +++ b/core/api-keys/src/graphql/schema.rs @@ -5,6 +5,7 @@ use crate::{app::ApiKeysApp, identity::IdentityApiKeyId}; pub struct AuthSubject { pub id: String, + pub read_only: bool, } #[derive(Clone, Copy)] @@ -121,6 +122,9 @@ impl Mutation { ) -> async_graphql::Result { let app = ctx.data_unchecked::(); let subject = ctx.data::()?; + if subject.read_only { + return Err("Permission denied".into()); + } let key = app .create_api_key_for_subject( &subject.id, diff --git a/core/api-keys/src/server/mod.rs b/core/api-keys/src/server/mod.rs index e33ac41a61..376c6754de 100644 --- a/core/api-keys/src/server/mod.rs +++ b/core/api-keys/src/server/mod.rs @@ -20,6 +20,8 @@ use jwks::*; pub struct JwtClaims { sub: String, exp: u64, + #[serde(default)] + read_only: String, } pub async fn run_server(config: ServerConfig, api_keys_app: ApiKeysApp) -> anyhow::Result<()> { @@ -76,7 +78,10 @@ pub async fn graphql_handler( ) -> GraphQLResponse { let req = req.into_inner(); schema - .execute(req.data(graphql::AuthSubject { id: jwt_claims.sub })) + .execute(req.data(graphql::AuthSubject { + id: jwt_claims.sub, + read_only: jwt_claims.read_only.eq("true"), + })) .await .into() } diff --git a/dev/config/ory/oathkeeper_rules.yaml b/dev/config/ory/oathkeeper_rules.yaml index 9f505d202f..5facf89fec 100644 --- a/dev/config/ory/oathkeeper_rules.yaml +++ b/dev/config/ory/oathkeeper_rules.yaml @@ -87,7 +87,7 @@ mutators: - handler: id_token config: #! TODO: add aud: {"aud": ["https://api/graphql"] } - claims: '{"sub": "{{ print .Subject }}", "session_id": "{{ print .Extra.id }}", "expires_at": "{{ print .Extra.expires_at }}", "scope": "{{ print .Extra.scope }}", "client_id": "{{ print .Extra.client_id }}" }' + claims: '{"sub": "{{ print .Subject }}", "session_id": "{{ print .Extra.id }}", "expires_at": "{{ print .Extra.expires_at }}", "scope": "{{ print .Extra.scope }}", "client_id": "{{ print .Extra.client_id }}", "read_only": "{{ print .Extra.read_only }}" }' - id: admin-backend upstream: From d78b26d8d41c5631b2271dce1e091812475e9647 Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Wed, 15 Nov 2023 16:15:37 +0100 Subject: [PATCH 4/8] chore: handle readOnly in core public api --- bats/core/api-keys/api-keys.bats | 11 +++++++++++ core/api/src/servers/graphql-main-server.ts | 10 +++++++++- core/api/src/servers/index.files.d.ts | 1 + core/api/src/servers/middlewares/session.ts | 2 ++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bats/core/api-keys/api-keys.bats b/bats/core/api-keys/api-keys.bats index 3aef409050..66632adf3b 100644 --- a/bats/core/api-keys/api-keys.bats +++ b/bats/core/api-keys/api-keys.bats @@ -96,8 +96,19 @@ new_key_name() { @test "api-keys: read-only key cannot mutate" { key_name="$(new_key_name)" + variables="{\"input\":{\"name\":\"${key_name}\"}}" exec_graphql 'api-key-secret' 'api-key-create' "$variables" errors="$(graphql_output '.errors | length')" [[ "${errors}" = "1" ]] || exit 1 + + variables="{\"input\":{\"currency\":\"USD\"}}" + exec_graphql 'api-key-secret' 'update-display-currency' "$variables" + errors="$(graphql_output '.errors | length')" + [[ "${errors}" = "1" ]] || exit 1 + + # Sanity check that it works with alice + exec_graphql 'alice' 'update-display-currency' "$variables" + errors="$(graphql_output '.errors | length')" + [[ "${errors}" = "0" ]] || exit 1 } diff --git a/core/api/src/servers/graphql-main-server.ts b/core/api/src/servers/graphql-main-server.ts index ad4b881f64..4bcd1ddcc3 100644 --- a/core/api/src/servers/graphql-main-server.ts +++ b/core/api/src/servers/graphql-main-server.ts @@ -43,6 +43,14 @@ export const isAuthenticated = rule({ cache: "contextual" })(( return "domainAccount" in ctx && !!ctx.domainAccount }) +export const isAuthenticatedForMutation = rule({ cache: "contextual" })(( + parent, + args, + ctx: GraphQLPublicContext, +) => { + return "domainAccount" in ctx && !!ctx.domainAccount && !ctx.readOnly +}) + const setGqlContext = async ( req: Request, res: Response, @@ -104,7 +112,7 @@ export async function startApolloServerForCoreSchema() { ...mutationFields.authed.atAccountLevel, ...mutationFields.authed.atWalletLevel, })) { - authedMutationFields[key] = isAuthenticated + authedMutationFields[key] = isAuthenticatedForMutation } const permissions = shield( diff --git a/core/api/src/servers/index.files.d.ts b/core/api/src/servers/index.files.d.ts index add09bc539..957d77cebe 100644 --- a/core/api/src/servers/index.files.d.ts +++ b/core/api/src/servers/index.files.d.ts @@ -11,6 +11,7 @@ type GraphQLPublicContext = { loaders: Loaders ip: IpAddress | undefined sessionId: SessionId | undefined + readOnly: boolean } type GraphQLPublicContextAuth = GraphQLPublicContext & { diff --git a/core/api/src/servers/middlewares/session.ts b/core/api/src/servers/middlewares/session.ts index f21b991801..f1b2cbd943 100644 --- a/core/api/src/servers/middlewares/session.ts +++ b/core/api/src/servers/middlewares/session.ts @@ -28,6 +28,7 @@ export const sessionPublicContext = async ({ const scope = tokenPayload?.scope?.split(" ") ?? [] const sub = tokenPayload?.sub const appId = tokenPayload?.client_id + const readOnly = tokenPayload?.read_only ?? false // note: value should match (ie: "anon") if not an accountId // settings from dev/ory/oathkeeper.yml/authenticator/anonymous/config/subjet @@ -85,5 +86,6 @@ export const sessionPublicContext = async ({ sessionId, scope, appId, + readOnly, } } From 4a17276d72eed81ff0c47c14d004a833c8054ad4 Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Wed, 15 Nov 2023 21:07:13 +0100 Subject: [PATCH 5/8] refactor: use scope for read only api keys --- core/api-keys/src/lib.rs | 1 + core/api-keys/src/scope.rs | 14 +++++ core/api-keys/src/server/mod.rs | 14 +++-- core/api/BUCK | 2 +- .../dev/apollo-federation/supergraph.graphql | 2 - core/api/src/domain/authorization/index.ts | 5 +- core/api/src/servers/graphql-main-server.ts | 10 +--- core/api/src/servers/index.files.d.ts | 1 - core/api/src/servers/middlewares/scope.ts | 60 +++++++++---------- core/api/src/servers/middlewares/session.ts | 6 +- dev/config/ory/oathkeeper_rules.yaml | 2 +- 11 files changed, 62 insertions(+), 55 deletions(-) create mode 100644 core/api-keys/src/scope.rs diff --git a/core/api-keys/src/lib.rs b/core/api-keys/src/lib.rs index f7745aa7b3..6208624fed 100644 --- a/core/api-keys/src/lib.rs +++ b/core/api-keys/src/lib.rs @@ -6,4 +6,5 @@ pub mod cli; mod entity; pub mod graphql; pub mod identity; +pub mod scope; pub mod server; diff --git a/core/api-keys/src/scope.rs b/core/api-keys/src/scope.rs new file mode 100644 index 0000000000..77bdab207c --- /dev/null +++ b/core/api-keys/src/scope.rs @@ -0,0 +1,14 @@ +pub const READ_SCOPE: &str = "read"; +pub const WRITE_SCOPE: &str = "write"; + +pub fn read_only_scope() -> String { + format!("{READ_SCOPE}") +} + +pub fn read_write_scope() -> String { + format!("{READ_SCOPE} {WRITE_SCOPE}") +} + +pub fn is_read_only(scope: &String) -> bool { + !(scope.as_str().split(" ").any(|s| s == WRITE_SCOPE) || scope.is_empty()) +} diff --git a/core/api-keys/src/server/mod.rs b/core/api-keys/src/server/mod.rs index 376c6754de..e93bfeefb3 100644 --- a/core/api-keys/src/server/mod.rs +++ b/core/api-keys/src/server/mod.rs @@ -21,7 +21,7 @@ pub struct JwtClaims { sub: String, exp: u64, #[serde(default)] - read_only: String, + scope: String, } pub async fn run_server(config: ServerConfig, api_keys_app: ApiKeysApp) -> anyhow::Result<()> { @@ -59,7 +59,7 @@ pub async fn run_server(config: ServerConfig, api_keys_app: ApiKeysApp) -> anyho #[derive(Debug, Serialize)] struct CheckResponse { sub: String, - read_only: bool, + scope: String, } async fn check_handler( @@ -68,7 +68,12 @@ async fn check_handler( ) -> Result, ApplicationError> { let key = headers.get(header).ok_or(ApplicationError::MissingApiKey)?; let (sub, read_only) = app.lookup_authenticated_subject(key.to_str()?).await?; - Ok(Json(CheckResponse { sub, read_only })) + let scope = if read_only { + crate::scope::read_only_scope() + } else { + crate::scope::read_write_scope() + }; + Ok(Json(CheckResponse { sub, scope })) } pub async fn graphql_handler( @@ -77,10 +82,11 @@ pub async fn graphql_handler( req: GraphQLRequest, ) -> GraphQLResponse { let req = req.into_inner(); + let read_only = crate::scope::is_read_only(&jwt_claims.scope); schema .execute(req.data(graphql::AuthSubject { id: jwt_claims.sub, - read_only: jwt_claims.read_only.eq("true"), + read_only, })) .await .into() diff --git a/core/api/BUCK b/core/api/BUCK index be001e3423..bb3358e1e8 100644 --- a/core/api/BUCK +++ b/core/api/BUCK @@ -13,7 +13,7 @@ load( load("@toolchains//rover:macros.bzl", "sdl", "diff_check", "dev_update_file") dev_pnpm_task_binary( - name = "lint-fix", + name = "fix-lint", command = "eslint-fix", ) diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index b0c019e4eb..861c96d64b 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -180,7 +180,6 @@ type ApiKey expired: Boolean! lastUsedAt: Timestamp expiresAt: Timestamp! - readOnly: Boolean! } input ApiKeyCreateInput @@ -188,7 +187,6 @@ input ApiKeyCreateInput { name: String! expireInDays: Int - readOnly: Boolean! = false } type ApiKeyCreatePayload diff --git a/core/api/src/domain/authorization/index.ts b/core/api/src/domain/authorization/index.ts index 6d96a519aa..aca3be2d50 100644 --- a/core/api/src/domain/authorization/index.ts +++ b/core/api/src/domain/authorization/index.ts @@ -1,6 +1,5 @@ export const ScopesOauth2 = { - TransactionsRead: "transactions:read", - Offline: "offline", - PaymentsSend: "payments:send", + Read: "read", + Write: "write", } as const export default ScopesOauth2 diff --git a/core/api/src/servers/graphql-main-server.ts b/core/api/src/servers/graphql-main-server.ts index 4bcd1ddcc3..ad4b881f64 100644 --- a/core/api/src/servers/graphql-main-server.ts +++ b/core/api/src/servers/graphql-main-server.ts @@ -43,14 +43,6 @@ export const isAuthenticated = rule({ cache: "contextual" })(( return "domainAccount" in ctx && !!ctx.domainAccount }) -export const isAuthenticatedForMutation = rule({ cache: "contextual" })(( - parent, - args, - ctx: GraphQLPublicContext, -) => { - return "domainAccount" in ctx && !!ctx.domainAccount && !ctx.readOnly -}) - const setGqlContext = async ( req: Request, res: Response, @@ -112,7 +104,7 @@ export async function startApolloServerForCoreSchema() { ...mutationFields.authed.atAccountLevel, ...mutationFields.authed.atWalletLevel, })) { - authedMutationFields[key] = isAuthenticatedForMutation + authedMutationFields[key] = isAuthenticated } const permissions = shield( diff --git a/core/api/src/servers/index.files.d.ts b/core/api/src/servers/index.files.d.ts index 957d77cebe..add09bc539 100644 --- a/core/api/src/servers/index.files.d.ts +++ b/core/api/src/servers/index.files.d.ts @@ -11,7 +11,6 @@ type GraphQLPublicContext = { loaders: Loaders ip: IpAddress | undefined sessionId: SessionId | undefined - readOnly: boolean } type GraphQLPublicContextAuth = GraphQLPublicContext & { diff --git a/core/api/src/servers/middlewares/scope.ts b/core/api/src/servers/middlewares/scope.ts index 9e4ea4c1de..d6d8fbdb5c 100644 --- a/core/api/src/servers/middlewares/scope.ts +++ b/core/api/src/servers/middlewares/scope.ts @@ -3,9 +3,9 @@ import { GraphQLResolveInfo, GraphQLFieldResolver } from "graphql" import ScopesOauth2 from "@/domain/authorization" import { AuthorizationError } from "@/domain/errors" import { mapError } from "@/graphql/error-map" -import { mutationFields } from "@/graphql/public" +import { mutationFields, queryFields } from "@/graphql/public" -const readTransactionsAuthorize = async ( +const readAuthorize = async ( resolve: GraphQLFieldResolver, parent: unknown, args: unknown, @@ -13,27 +13,21 @@ const readTransactionsAuthorize = async ( info: GraphQLResolveInfo, ) => { const scope = context.scope - const appId = context.appId - - // not a delegated token - if (appId === undefined || appId === "") { - return resolve(parent, args, context, info) - } + console.log("SCOPE ", scope) + // not a token with scope if (scope === undefined || scope.length === 0) { - return mapError( - new AuthorizationError("appId is defined but scope is undefined or empty"), - ) + return resolve(parent, args, context, info) } - if (scope.find((s) => s === ScopesOauth2.TransactionsRead) !== undefined) { + if (scope.find((s) => s === ScopesOauth2.Read) !== undefined) { return resolve(parent, args, context, info) } - return mapError(new AuthorizationError("not authorized to read transactions")) + return mapError(new AuthorizationError("not authorized to read data")) } -const paymentSendAuthorize = async ( +const writeAuthorize = async ( resolve: GraphQLFieldResolver, parent: unknown, args: unknown, @@ -41,26 +35,21 @@ const paymentSendAuthorize = async ( info: GraphQLResolveInfo, ) => { const scope = context.scope - const appId = context.appId - // not a delegated token - if (appId === undefined || appId === "") { + // not a token with scope + if (scope === undefined || scope.length === 0) { return resolve(parent, args, context, info) } - if (scope === undefined) { - return mapError(new AuthorizationError("appId is defined but scope is undefined")) - } - - if (scope.find((s) => s === ScopesOauth2.PaymentsSend) !== undefined) { + if (scope.find((s) => s === ScopesOauth2.Write) !== undefined) { return resolve(parent, args, context, info) } - return mapError(new AuthorizationError("not authorized to send payments")) + return mapError(new AuthorizationError("not authorized to execute mutations")) } // Placed here because 'GraphQLFieldResolver' not working from .d.ts file -type ValidateWalletIdFn = ( +type ValidateFn = ( resolve: GraphQLFieldResolver, parent: unknown, args: unknown, @@ -68,14 +57,23 @@ type ValidateWalletIdFn = ( info: GraphQLResolveInfo, ) => Promise -const walletIdMutationFields: { [key: string]: ValidateWalletIdFn } = {} -for (const key of Object.keys(mutationFields.authed.atWalletLevel)) { - walletIdMutationFields[key] = paymentSendAuthorize +const authedQueryFields: { [key: string]: ValidateFn } = {} +for (const key of Object.keys({ + ...queryFields.authed.atAccountLevel, + ...queryFields.authed.atWalletLevel, +})) { + authedQueryFields[key] = readAuthorize +} + +const authedMutationFields: { [key: string]: ValidateFn } = {} +for (const key of Object.keys({ + ...mutationFields.authed.atAccountLevel, + ...mutationFields.authed.atWalletLevel, +})) { + authedMutationFields[key] = writeAuthorize } export const scopeMiddleware = { - Query: { - me: readTransactionsAuthorize, - }, - Mutation: walletIdMutationFields, + Query: authedQueryFields, + Mutation: authedMutationFields, } diff --git a/core/api/src/servers/middlewares/session.ts b/core/api/src/servers/middlewares/session.ts index f1b2cbd943..b0c9a8bd5b 100644 --- a/core/api/src/servers/middlewares/session.ts +++ b/core/api/src/servers/middlewares/session.ts @@ -25,10 +25,11 @@ export const sessionPublicContext = async ({ const sessionId = tokenPayload?.session_id const expiresAt = tokenPayload?.expires_at - const scope = tokenPayload?.scope?.split(" ") ?? [] + const scope = (tokenPayload?.scope?.split(" ") ?? []).filter( + (element: string) => element !== "", + ) const sub = tokenPayload?.sub const appId = tokenPayload?.client_id - const readOnly = tokenPayload?.read_only ?? false // note: value should match (ie: "anon") if not an accountId // settings from dev/ory/oathkeeper.yml/authenticator/anonymous/config/subjet @@ -86,6 +87,5 @@ export const sessionPublicContext = async ({ sessionId, scope, appId, - readOnly, } } diff --git a/dev/config/ory/oathkeeper_rules.yaml b/dev/config/ory/oathkeeper_rules.yaml index 5facf89fec..3832f4a514 100644 --- a/dev/config/ory/oathkeeper_rules.yaml +++ b/dev/config/ory/oathkeeper_rules.yaml @@ -87,7 +87,7 @@ mutators: - handler: id_token config: #! TODO: add aud: {"aud": ["https://api/graphql"] } - claims: '{"sub": "{{ print .Subject }}", "session_id": "{{ print .Extra.id }}", "expires_at": "{{ print .Extra.expires_at }}", "scope": "{{ print .Extra.scope }}", "client_id": "{{ print .Extra.client_id }}", "read_only": "{{ print .Extra.read_only }}" }' + claims: '{"sub": "{{ print .Subject }}", "session_id": "{{ print .Extra.id }}", "expires_at": "{{ print .Extra.expires_at }}", "scope": "{{ print .Extra.scope }}", "client_id": "{{ print .Extra.client_id }}"}' - id: admin-backend upstream: From afcebcdfc0a731123be13b41cb9bf8ab64266f25 Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Thu, 16 Nov 2023 14:30:07 +0100 Subject: [PATCH 6/8] refactor: use read / write scopes everywhere --- apps/dashboard/app/api/auth/[...nextauth]/route.ts | 2 +- core/api/src/domain/authorization/index.ts | 1 + core/api/src/servers/middlewares/scope.ts | 1 - dev/bin/setup-hydra-client.sh | 4 ++-- docs/hydra.md | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/dashboard/app/api/auth/[...nextauth]/route.ts b/apps/dashboard/app/api/auth/[...nextauth]/route.ts index fa3c1e1fed..73f1e9e22c 100644 --- a/apps/dashboard/app/api/auth/[...nextauth]/route.ts +++ b/apps/dashboard/app/api/auth/[...nextauth]/route.ts @@ -26,7 +26,7 @@ export const authOptions: AuthOptions = { clientSecret: env.CLIENT_SECRET, wellKnown: `${env.HYDRA_PUBLIC}/.well-known/openid-configuration`, authorization: { - params: { scope: "offline transactions:read payments:send" }, + params: { scope: "offline read write" }, }, idToken: false, name: "Blink", diff --git a/core/api/src/domain/authorization/index.ts b/core/api/src/domain/authorization/index.ts index aca3be2d50..2044d6803d 100644 --- a/core/api/src/domain/authorization/index.ts +++ b/core/api/src/domain/authorization/index.ts @@ -1,4 +1,5 @@ export const ScopesOauth2 = { + Offline: "offline", Read: "read", Write: "write", } as const diff --git a/core/api/src/servers/middlewares/scope.ts b/core/api/src/servers/middlewares/scope.ts index d6d8fbdb5c..398fb46ede 100644 --- a/core/api/src/servers/middlewares/scope.ts +++ b/core/api/src/servers/middlewares/scope.ts @@ -14,7 +14,6 @@ const readAuthorize = async ( ) => { const scope = context.scope - console.log("SCOPE ", scope) // not a token with scope if (scope === undefined || scope.length === 0) { return resolve(parent, args, context, info) diff --git a/dev/bin/setup-hydra-client.sh b/dev/bin/setup-hydra-client.sh index 1f7072b3c4..2651abbd33 100755 --- a/dev/bin/setup-hydra-client.sh +++ b/dev/bin/setup-hydra-client.sh @@ -19,13 +19,13 @@ hydra_cli create client \ --grant-type "$grant_type" \ --response-type code,id_token \ --format json \ - --scope offline --scope transactions:read --scope payments:send \ + --scope offline --scope read --scope write \ --redirect-uri "$redirect_uri" > "${HYDRA_CLIENT_JSON}" CLIENT_ID=$(jq -r '.client_id' < "${HYDRA_CLIENT_JSON}") CLIENT_SECRET=$(jq -r '.client_secret' < "${HYDRA_CLIENT_JSON}") -AUTHORIZATION_URL="${HYDRA_PUBLIC_API}/oauth2/auth?client_id=$CLIENT_ID&scope=offline%20transactions:read&response_type=code&redirect_uri=$redirect_uri&state=kfISr3GhH0rqheByU6A6hqIG_f14pCGkZLSCUTHnvlI" +AUTHORIZATION_URL="${HYDRA_PUBLIC_API}/oauth2/auth?client_id=$CLIENT_ID&scope=offline%20read&response_type=code&redirect_uri=$redirect_uri&state=kfISr3GhH0rqheByU6A6hqIG_f14pCGkZLSCUTHnvlI" echo "export CLIENT_ID=$CLIENT_ID" > "${HYDRA_CLIENT_ENV}" echo "export CLIENT_SECRET=$CLIENT_SECRET" >> "${HYDRA_CLIENT_ENV}" diff --git a/docs/hydra.md b/docs/hydra.md index d972266b08..d353d4c475 100644 --- a/docs/hydra.md +++ b/docs/hydra.md @@ -54,8 +54,8 @@ code_client=$(hydra create client \ --response-type code,id_token \ --format json \ --scope offline \ - --scope transactions:read \ - --scope payments:send \ + --scope read \ + --scope write \ --redirect-uri $NEXTAUTH_URL/api/auth/callback/blink \ --skip-consent ) @@ -69,8 +69,8 @@ code_client_app_=$(hydra create client \ --grant-type authorization_code \ --response-type code,id_token \ --format json \ - --scope transactions:read \ - --scope payments:send \ + --scope read \ + --scope write \ --scope openid \ --redirect-uri http://localhost:3001/keys/create/callback \ --skip-consent @@ -93,7 +93,7 @@ code_client=$(hydra create client \ --grant-type authorization_code \ --response-type code,id_token \ --format json \ - --scope offline --scope transactions:read --scope payments:send \ + --scope offline --scope read --scope write \ --redirect-uri http://localhost:5555/callback \ --token-endpoint-auth-method none \ ) @@ -112,7 +112,7 @@ hydra perform authorization-code \ --client-secret $CLIENT_SECRET \ --endpoint http://localhost:4444/ \ --port 5555 \ - --scope offline --scope transactions:read --scope payments:send + --scope offline --scope read --scope write ``` do the login and consent From 43e8dcd0f405575f273c3e24356ee346c9bff73b Mon Sep 17 00:00:00 2001 From: Siddharth Date: Fri, 17 Nov 2023 00:42:09 +0530 Subject: [PATCH 7/8] feat(dasboard): added api ket scope in dashboard (#3560) Co-authored-by: Siddharth --- apps/dashboard/app/api-keys/server-actions.ts | 4 +- .../components/api-keys/api-card.tsx | 22 ++- apps/dashboard/components/api-keys/create.tsx | 14 +- apps/dashboard/components/api-keys/list.tsx | 89 +++++----- apps/dashboard/components/api-keys/utils.ts | 4 + apps/dashboard/services/graphql/generated.ts | 166 +++++++++++++++++- .../services/graphql/mutations/api-keys.ts | 3 +- .../services/graphql/queries/api-keys.ts | 1 + 8 files changed, 249 insertions(+), 54 deletions(-) diff --git a/apps/dashboard/app/api-keys/server-actions.ts b/apps/dashboard/app/api-keys/server-actions.ts index 96eeb4b403..4bff5aa437 100644 --- a/apps/dashboard/app/api-keys/server-actions.ts +++ b/apps/dashboard/app/api-keys/server-actions.ts @@ -47,6 +47,8 @@ export const revokeApiKeyServerAction = async (id: string) => { export const createApiKeyServerAction = async (_prevState: unknown, form: FormData) => { const apiKeyName = form.get("apiKeyName") + const readOnly = form.get("apiScope") === "readOnly" + if (!apiKeyName || typeof apiKeyName !== "string") { return { error: true, @@ -72,7 +74,7 @@ export const createApiKeyServerAction = async (_prevState: unknown, form: FormDa let data try { - data = await createApiKey(token, apiKeyName, apiKeyExpiresInDays) + data = await createApiKey(token, apiKeyName, apiKeyExpiresInDays, readOnly) } catch (err) { console.log("error in createApiKey ", err) return { diff --git a/apps/dashboard/components/api-keys/api-card.tsx b/apps/dashboard/components/api-keys/api-card.tsx index 3e32624668..bdb54b8cc0 100644 --- a/apps/dashboard/components/api-keys/api-card.tsx +++ b/apps/dashboard/components/api-keys/api-card.tsx @@ -3,16 +3,18 @@ import React from "react" import { Card, Divider, Typography, Box } from "@mui/joy" import RevokeKey from "./revoke" -import { formatDate } from "./utils" +import { formatDate, getScopeText } from "./utils" interface ApiKey { - id: string - name: string - createdAt: number - expiresAt: number - lastUsedAt?: number | null | undefined - expired: boolean - revoked: boolean + readonly __typename: "ApiKey" + readonly id: string + readonly name: string + readonly createdAt: number + readonly revoked: boolean + readonly expired: boolean + readonly lastUsedAt?: number | null + readonly expiresAt: number + readonly readOnly: boolean } interface ApiKeysCardProps { @@ -47,6 +49,10 @@ const ApiKeysCard: React.FC = ({ Expires At {formatDate(key.expiresAt)} + + Scope + {getScopeText(key.readOnly)} + {!key.revoked && !key.expired && } )) diff --git a/apps/dashboard/components/api-keys/create.tsx b/apps/dashboard/components/api-keys/create.tsx index e59e7aaa1e..087b53743c 100644 --- a/apps/dashboard/components/api-keys/create.tsx +++ b/apps/dashboard/components/api-keys/create.tsx @@ -13,6 +13,8 @@ import { Tooltip, Select, Option, + Radio, + RadioGroup, } from "@mui/joy" import InfoOutlined from "@mui/icons-material/InfoOutlined" @@ -254,7 +256,17 @@ const ApiKeyCreate = () => { {state.message} ) : null} - + + Scope + + + + Full access: read and write account details. + + + Limited access: view data only. + + = ({ Name - API Key ID - Expires At - Last Used - Action + API Key ID + Scope + Expires At + Last Used + Action - {activeKeys.map(({ id, name, expiresAt, lastUsedAt }) => ( - - {name} - {id} - {formatDate(expiresAt)} - - {lastUsedAt ? formatDate(lastUsedAt) : "Never"} - - - - - - ))} + {activeKeys.map(({ id, name, expiresAt, lastUsedAt, readOnly }) => { + return ( + + {name} + {id} + {getScopeText(readOnly)} + {formatDate(expiresAt)} + {lastUsedAt ? formatDate(lastUsedAt) : "Never"} + + + + + ) + })} {activeKeys.length === 0 && No active keys to display.} @@ -62,19 +68,21 @@ const ApiKeysList: React.FC = ({ - - - + + + + - {revokedKeys.map(({ id, name, createdAt }) => ( + {revokedKeys.map(({ id, name, createdAt, readOnly }) => ( - - - - + + + + + ))} @@ -83,25 +91,26 @@ const ApiKeysList: React.FC = ({ + {/* Expired Keys Section */} Expired Keys
NameAPI Key IDCreated AtNameAPI Key IDScopeCreated At Status
{name}{id}{formatDate(createdAt)}Revoked{name}{id}{getScopeText(readOnly)}{formatDate(createdAt)}Revoked
- - + + + - {expiredKeys.map(({ id, name, createdAt, expiresAt }) => ( + {expiredKeys.map(({ id, name, createdAt, expiresAt, readOnly }) => ( - - - - + + + + + ))} diff --git a/apps/dashboard/components/api-keys/utils.ts b/apps/dashboard/components/api-keys/utils.ts index 78ad57b62f..e8890fd876 100644 --- a/apps/dashboard/components/api-keys/utils.ts +++ b/apps/dashboard/components/api-keys/utils.ts @@ -6,3 +6,7 @@ export const formatDate = (timestamp: number): string => { } return new Date(timestamp * 1000).toLocaleDateString(undefined, options) } + +export const getScopeText = (readOnly: boolean): string => { + return readOnly ? "Read Only" : "Read and Write" +} diff --git a/apps/dashboard/services/graphql/generated.ts b/apps/dashboard/services/graphql/generated.ts index 2ebabae358..cf7e6c4ef2 100644 --- a/apps/dashboard/services/graphql/generated.ts +++ b/apps/dashboard/services/graphql/generated.ts @@ -90,9 +90,11 @@ export type Account = { readonly defaultWalletId: Scalars['WalletId']['output']; readonly displayCurrency: Scalars['DisplayCurrency']['output']; readonly id: Scalars['ID']['output']; + readonly invoices?: Maybe; readonly level: AccountLevel; readonly limits: AccountLimits; readonly notificationSettings: NotificationSettings; + readonly pendingIncomingTransactions: ReadonlyArray; readonly realtimePrice: RealtimePrice; readonly transactions?: Maybe; readonly walletById: Wallet; @@ -105,6 +107,20 @@ export type AccountCsvTransactionsArgs = { }; +export type AccountInvoicesArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + walletIds?: InputMaybe>>; +}; + + +export type AccountPendingIncomingTransactionsArgs = { + walletIds?: InputMaybe>>; +}; + + export type AccountTransactionsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -202,12 +218,14 @@ export type ApiKey = { readonly id: Scalars['ID']['output']; readonly lastUsedAt?: Maybe; readonly name: Scalars['String']['output']; + readonly readOnly: Scalars['Boolean']['output']; readonly revoked: Scalars['Boolean']['output']; }; export type ApiKeyCreateInput = { readonly expireInDays?: InputMaybe; readonly name: Scalars['String']['input']; + readonly readOnly?: Scalars['Boolean']['input']; }; export type ApiKeyCreatePayload = { @@ -240,8 +258,12 @@ export type BtcWallet = Wallet & { readonly balance: Scalars['SignedAmount']['output']; readonly id: Scalars['ID']['output']; readonly invoiceByPaymentHash: Invoice; + /** A list of all invoices associated with walletIds optionally passed. */ + readonly invoices?: Maybe; /** An unconfirmed incoming onchain balance. */ readonly pendingIncomingBalance: Scalars['SignedAmount']['output']; + readonly pendingIncomingTransactions: ReadonlyArray; + readonly pendingIncomingTransactionsByAddress: ReadonlyArray; readonly transactionById: Transaction; /** A list of BTC transactions associated with this wallet. */ readonly transactions?: Maybe; @@ -257,6 +279,21 @@ export type BtcWalletInvoiceByPaymentHashArgs = { }; +/** A wallet belonging to an account which contains a BTC balance and a list of transactions. */ +export type BtcWalletInvoicesArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +/** A wallet belonging to an account which contains a BTC balance and a list of transactions. */ +export type BtcWalletPendingIncomingTransactionsByAddressArgs = { + address: Scalars['OnChainAddress']['input']; +}; + + /** A wallet belonging to an account which contains a BTC balance and a list of transactions. */ export type BtcWalletTransactionByIdArgs = { transactionId: Scalars['ID']['input']; @@ -350,9 +387,12 @@ export type ConsumerAccount = Account & { readonly defaultWalletId: Scalars['WalletId']['output']; readonly displayCurrency: Scalars['DisplayCurrency']['output']; readonly id: Scalars['ID']['output']; + /** A list of all invoices associated with walletIds optionally passed. */ + readonly invoices?: Maybe; readonly level: AccountLevel; readonly limits: AccountLimits; readonly notificationSettings: NotificationSettings; + readonly pendingIncomingTransactions: ReadonlyArray; /** List the quiz questions of the consumer account */ readonly quiz: ReadonlyArray; readonly realtimePrice: RealtimePrice; @@ -368,6 +408,20 @@ export type ConsumerAccountCsvTransactionsArgs = { }; +export type ConsumerAccountInvoicesArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; + walletIds?: InputMaybe>>; +}; + + +export type ConsumerAccountPendingIncomingTransactionsArgs = { + walletIds?: InputMaybe>>; +}; + + export type ConsumerAccountTransactionsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -518,6 +572,7 @@ export type IntraLedgerUsdPaymentSendInput = { /** A lightning invoice. */ export type Invoice = { + readonly createdAt: Scalars['Timestamp']['output']; /** The payment hash of the lightning invoice. */ readonly paymentHash: Scalars['PaymentHash']['output']; /** The bolt11 invoice to be paid. */ @@ -528,6 +583,24 @@ export type Invoice = { readonly paymentStatus: InvoicePaymentStatus; }; +/** A connection to a list of items. */ +export type InvoiceConnection = { + readonly __typename: 'InvoiceConnection'; + /** A list of edges. */ + readonly edges?: Maybe>; + /** Information to aid in pagination. */ + readonly pageInfo: PageInfo; +}; + +/** An edge in a connection. */ +export type InvoiceEdge = { + readonly __typename: 'InvoiceEdge'; + /** A cursor for use in pagination */ + readonly cursor: Scalars['String']['output']; + /** The item at the end of the edge */ + readonly node: Invoice; +}; + export const InvoicePaymentStatus = { Expired: 'EXPIRED', Paid: 'PAID', @@ -537,11 +610,12 @@ export const InvoicePaymentStatus = { export type InvoicePaymentStatus = typeof InvoicePaymentStatus[keyof typeof InvoicePaymentStatus]; export type LnInvoice = Invoice & { readonly __typename: 'LnInvoice'; + readonly createdAt: Scalars['Timestamp']['output']; readonly paymentHash: Scalars['PaymentHash']['output']; readonly paymentRequest: Scalars['LnPaymentRequest']['output']; readonly paymentSecret: Scalars['LnPaymentSecret']['output']; readonly paymentStatus: InvoicePaymentStatus; - readonly satoshis?: Maybe; + readonly satoshis: Scalars['SatAmount']['output']; }; export type LnInvoiceCreateInput = { @@ -599,6 +673,7 @@ export type LnInvoicePaymentStatusPayload = { export type LnNoAmountInvoice = Invoice & { readonly __typename: 'LnNoAmountInvoice'; + readonly createdAt: Scalars['Timestamp']['output']; readonly paymentHash: Scalars['PaymentHash']['output']; readonly paymentRequest: Scalars['LnPaymentRequest']['output']; readonly paymentSecret: Scalars['LnPaymentSecret']['output']; @@ -1587,8 +1662,12 @@ export type UsdWallet = Wallet & { readonly balance: Scalars['SignedAmount']['output']; readonly id: Scalars['ID']['output']; readonly invoiceByPaymentHash: Invoice; + /** A list of all invoices associated with walletIds optionally passed. */ + readonly invoices?: Maybe; /** An unconfirmed incoming onchain balance. */ readonly pendingIncomingBalance: Scalars['SignedAmount']['output']; + readonly pendingIncomingTransactions: ReadonlyArray; + readonly pendingIncomingTransactionsByAddress: ReadonlyArray; readonly transactionById: Transaction; readonly transactions?: Maybe; readonly transactionsByAddress?: Maybe; @@ -1603,6 +1682,21 @@ export type UsdWalletInvoiceByPaymentHashArgs = { }; +/** A wallet belonging to an account which contains a USD balance and a list of transactions. */ +export type UsdWalletInvoicesArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +/** A wallet belonging to an account which contains a USD balance and a list of transactions. */ +export type UsdWalletPendingIncomingTransactionsByAddressArgs = { + address: Scalars['OnChainAddress']['input']; +}; + + /** A wallet belonging to an account which contains a USD balance and a list of transactions. */ export type UsdWalletTransactionByIdArgs = { transactionId: Scalars['ID']['input']; @@ -1844,7 +1938,22 @@ export type Wallet = { readonly balance: Scalars['SignedAmount']['output']; readonly id: Scalars['ID']['output']; readonly invoiceByPaymentHash: Invoice; + readonly invoices?: Maybe; readonly pendingIncomingBalance: Scalars['SignedAmount']['output']; + /** + * Pending incoming OnChain transactions. When transactions + * are confirmed they will receive a new id and be found in the transactions + * list. Transactions are ordered anti-chronologically, + * ie: the newest transaction will be first + */ + readonly pendingIncomingTransactions: ReadonlyArray; + /** + * Pending incoming OnChain transactions. When transactions + * are confirmed they will receive a new id and be found in the transactions + * list. Transactions are ordered anti-chronologically, + * ie: the newest transaction will be first + */ + readonly pendingIncomingTransactionsByAddress: ReadonlyArray; readonly transactionById: Transaction; /** * Transactions are ordered anti-chronologically, @@ -1868,6 +1977,21 @@ export type WalletInvoiceByPaymentHashArgs = { }; +/** A generic wallet which stores value in one of our supported currencies. */ +export type WalletInvoicesArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + +/** A generic wallet which stores value in one of our supported currencies. */ +export type WalletPendingIncomingTransactionsByAddressArgs = { + address: Scalars['OnChainAddress']['input']; +}; + + /** A generic wallet which stores value in one of our supported currencies. */ export type WalletTransactionByIdArgs = { transactionId: Scalars['ID']['input']; @@ -1954,7 +2078,7 @@ export type UserEmailDeleteMutation = { readonly __typename: 'Mutation', readonl export type ApiKeysQueryVariables = Exact<{ [key: string]: never; }>; -export type ApiKeysQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly apiKeys: ReadonlyArray<{ readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt: number }> } | null }; +export type ApiKeysQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly apiKeys: ReadonlyArray<{ readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt: number, readonly readOnly: boolean }> } | null }; export type CallbackEndpointsQueryVariables = Exact<{ [key: string]: never; }>; @@ -2259,6 +2383,7 @@ export const ApiKeysDocument = gql` expired lastUsedAt expiresAt + readOnly } } } @@ -2726,6 +2851,8 @@ export type ResolversTypes = { IntraLedgerUpdate: ResolverTypeWrapper; IntraLedgerUsdPaymentSendInput: IntraLedgerUsdPaymentSendInput; Invoice: ResolverTypeWrapper['Invoice']>; + InvoiceConnection: ResolverTypeWrapper; + InvoiceEdge: ResolverTypeWrapper; InvoicePaymentStatus: InvoicePaymentStatus; Language: ResolverTypeWrapper; LnInvoice: ResolverTypeWrapper; @@ -2928,6 +3055,8 @@ export type ResolversParentTypes = { IntraLedgerUpdate: IntraLedgerUpdate; IntraLedgerUsdPaymentSendInput: IntraLedgerUsdPaymentSendInput; Invoice: ResolversInterfaceTypes['Invoice']; + InvoiceConnection: InvoiceConnection; + InvoiceEdge: InvoiceEdge; Language: Scalars['Language']['output']; LnInvoice: LnInvoice; LnInvoiceCreateInput: LnInvoiceCreateInput; @@ -3066,9 +3195,11 @@ export type AccountResolvers; displayCurrency?: Resolver; id?: Resolver; + invoices?: Resolver, ParentType, ContextType, Partial>; level?: Resolver; limits?: Resolver; notificationSettings?: Resolver; + pendingIncomingTransactions?: Resolver, ParentType, ContextType, Partial>; realtimePrice?: Resolver; transactions?: Resolver, ParentType, ContextType, Partial>; walletById?: Resolver>; @@ -3120,6 +3251,7 @@ export type ApiKeyResolvers; lastUsedAt?: Resolver, ParentType, ContextType>; name?: Resolver; + readOnly?: Resolver; revoked?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -3151,7 +3283,10 @@ export type BtcWalletResolvers; id?: Resolver; invoiceByPaymentHash?: Resolver>; + invoices?: Resolver, ParentType, ContextType, Partial>; pendingIncomingBalance?: Resolver; + pendingIncomingTransactions?: Resolver, ParentType, ContextType>; + pendingIncomingTransactionsByAddress?: Resolver, ParentType, ContextType, RequireFields>; transactionById?: Resolver>; transactions?: Resolver, ParentType, ContextType, Partial>; transactionsByAddress?: Resolver, ParentType, ContextType, RequireFields>; @@ -3208,9 +3343,11 @@ export type ConsumerAccountResolvers; displayCurrency?: Resolver; id?: Resolver; + invoices?: Resolver, ParentType, ContextType, Partial>; level?: Resolver; limits?: Resolver; notificationSettings?: Resolver; + pendingIncomingTransactions?: Resolver, ParentType, ContextType, Partial>; quiz?: Resolver, ParentType, ContextType>; realtimePrice?: Resolver; transactions?: Resolver, ParentType, ContextType, Partial>; @@ -3350,22 +3487,36 @@ export type IntraLedgerUpdateResolvers = { __resolveType: TypeResolveFn<'LnInvoice' | 'LnNoAmountInvoice', ParentType, ContextType>; + createdAt?: Resolver; paymentHash?: Resolver; paymentRequest?: Resolver; paymentSecret?: Resolver; paymentStatus?: Resolver; }; +export type InvoiceConnectionResolvers = { + edges?: Resolver>, ParentType, ContextType>; + pageInfo?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type InvoiceEdgeResolvers = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export interface LanguageScalarConfig extends GraphQLScalarTypeConfig { name: 'Language'; } export type LnInvoiceResolvers = { + createdAt?: Resolver; paymentHash?: Resolver; paymentRequest?: Resolver; paymentSecret?: Resolver; paymentStatus?: Resolver; - satoshis?: Resolver, ParentType, ContextType>; + satoshis?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -3382,6 +3533,7 @@ export type LnInvoicePaymentStatusPayloadResolvers = { + createdAt?: Resolver; paymentHash?: Resolver; paymentRequest?: Resolver; paymentSecret?: Resolver; @@ -3812,7 +3964,10 @@ export type UsdWalletResolvers; id?: Resolver; invoiceByPaymentHash?: Resolver>; + invoices?: Resolver, ParentType, ContextType, Partial>; pendingIncomingBalance?: Resolver; + pendingIncomingTransactions?: Resolver, ParentType, ContextType>; + pendingIncomingTransactionsByAddress?: Resolver, ParentType, ContextType, RequireFields>; transactionById?: Resolver>; transactions?: Resolver, ParentType, ContextType, Partial>; transactionsByAddress?: Resolver, ParentType, ContextType, RequireFields>; @@ -3934,7 +4089,10 @@ export type WalletResolvers; id?: Resolver; invoiceByPaymentHash?: Resolver>; + invoices?: Resolver, ParentType, ContextType, Partial>; pendingIncomingBalance?: Resolver; + pendingIncomingTransactions?: Resolver, ParentType, ContextType>; + pendingIncomingTransactionsByAddress?: Resolver, ParentType, ContextType, RequireFields>; transactionById?: Resolver>; transactions?: Resolver, ParentType, ContextType, Partial>; transactionsByAddress?: Resolver, ParentType, ContextType, RequireFields>; @@ -3992,6 +4150,8 @@ export type Resolvers = { InitiationViaOnChain?: InitiationViaOnChainResolvers; IntraLedgerUpdate?: IntraLedgerUpdateResolvers; Invoice?: InvoiceResolvers; + InvoiceConnection?: InvoiceConnectionResolvers; + InvoiceEdge?: InvoiceEdgeResolvers; Language?: GraphQLScalarType; LnInvoice?: LnInvoiceResolvers; LnInvoicePayload?: LnInvoicePayloadResolvers; diff --git a/apps/dashboard/services/graphql/mutations/api-keys.ts b/apps/dashboard/services/graphql/mutations/api-keys.ts index 9eacb31e2f..e2d5d64f06 100644 --- a/apps/dashboard/services/graphql/mutations/api-keys.ts +++ b/apps/dashboard/services/graphql/mutations/api-keys.ts @@ -43,12 +43,13 @@ export async function createApiKey( token: string, name: string, expireInDays: number | null, + readOnly: boolean, ) { const client = apollo(token).getClient() try { const { data } = await client.mutate({ mutation: ApiKeyCreateDocument, - variables: { input: { name, expireInDays } }, + variables: { input: { name, expireInDays, readOnly } }, }) return data } catch (error) { diff --git a/apps/dashboard/services/graphql/queries/api-keys.ts b/apps/dashboard/services/graphql/queries/api-keys.ts index 78ae8bbce7..ddbc99e6f9 100644 --- a/apps/dashboard/services/graphql/queries/api-keys.ts +++ b/apps/dashboard/services/graphql/queries/api-keys.ts @@ -14,6 +14,7 @@ gql` expired lastUsedAt expiresAt + readOnly } } } From 836d591a86a16df95ef07dd864a76dae86059c2a Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Thu, 16 Nov 2023 20:14:17 +0100 Subject: [PATCH 8/8] chore: remove offline scope --- apps/dashboard/app/api/auth/[...nextauth]/route.ts | 2 +- core/api/src/domain/authorization/index.ts | 1 - dev/bin/setup-hydra-client.sh | 4 ++-- docs/hydra.md | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/api/auth/[...nextauth]/route.ts b/apps/dashboard/app/api/auth/[...nextauth]/route.ts index 73f1e9e22c..77ed74eec4 100644 --- a/apps/dashboard/app/api/auth/[...nextauth]/route.ts +++ b/apps/dashboard/app/api/auth/[...nextauth]/route.ts @@ -26,7 +26,7 @@ export const authOptions: AuthOptions = { clientSecret: env.CLIENT_SECRET, wellKnown: `${env.HYDRA_PUBLIC}/.well-known/openid-configuration`, authorization: { - params: { scope: "offline read write" }, + params: { scope: "read write" }, }, idToken: false, name: "Blink", diff --git a/core/api/src/domain/authorization/index.ts b/core/api/src/domain/authorization/index.ts index 2044d6803d..aca3be2d50 100644 --- a/core/api/src/domain/authorization/index.ts +++ b/core/api/src/domain/authorization/index.ts @@ -1,5 +1,4 @@ export const ScopesOauth2 = { - Offline: "offline", Read: "read", Write: "write", } as const diff --git a/dev/bin/setup-hydra-client.sh b/dev/bin/setup-hydra-client.sh index 2651abbd33..b60471d870 100755 --- a/dev/bin/setup-hydra-client.sh +++ b/dev/bin/setup-hydra-client.sh @@ -19,13 +19,13 @@ hydra_cli create client \ --grant-type "$grant_type" \ --response-type code,id_token \ --format json \ - --scope offline --scope read --scope write \ + --scope read --scope write \ --redirect-uri "$redirect_uri" > "${HYDRA_CLIENT_JSON}" CLIENT_ID=$(jq -r '.client_id' < "${HYDRA_CLIENT_JSON}") CLIENT_SECRET=$(jq -r '.client_secret' < "${HYDRA_CLIENT_JSON}") -AUTHORIZATION_URL="${HYDRA_PUBLIC_API}/oauth2/auth?client_id=$CLIENT_ID&scope=offline%20read&response_type=code&redirect_uri=$redirect_uri&state=kfISr3GhH0rqheByU6A6hqIG_f14pCGkZLSCUTHnvlI" +AUTHORIZATION_URL="${HYDRA_PUBLIC_API}/oauth2/auth?client_id=$CLIENT_ID&scope=read&response_type=code&redirect_uri=$redirect_uri&state=kfISr3GhH0rqheByU6A6hqIG_f14pCGkZLSCUTHnvlI" echo "export CLIENT_ID=$CLIENT_ID" > "${HYDRA_CLIENT_ENV}" echo "export CLIENT_SECRET=$CLIENT_SECRET" >> "${HYDRA_CLIENT_ENV}" diff --git a/docs/hydra.md b/docs/hydra.md index d353d4c475..f0657fc4bc 100644 --- a/docs/hydra.md +++ b/docs/hydra.md @@ -93,7 +93,7 @@ code_client=$(hydra create client \ --grant-type authorization_code \ --response-type code,id_token \ --format json \ - --scope offline --scope read --scope write \ + --scope read --scope write \ --redirect-uri http://localhost:5555/callback \ --token-endpoint-auth-method none \ ) @@ -112,7 +112,7 @@ hydra perform authorization-code \ --client-secret $CLIENT_SECRET \ --endpoint http://localhost:4444/ \ --port 5555 \ - --scope offline --scope read --scope write + --scope read --scope write ``` do the login and consent
NameAPI Key IDNameAPI Key IDScope Created At Expires At
{name}{id}{formatDate(createdAt)} - {formatDate(expiresAt)} - {name}{id}{getScopeText(readOnly)}{formatDate(createdAt)}{formatDate(expiresAt)}