You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-21 23:00:50 +03:00
Test the admin scope on the GraphQL API
This commit is contained in:
@@ -13,26 +13,141 @@
|
||||
// limitations under the License.
|
||||
|
||||
use axum::http::Request;
|
||||
use chrono::Duration;
|
||||
use hyper::StatusCode;
|
||||
use mas_data_model::AuthorizationCode;
|
||||
use mas_router::SimpleRoute;
|
||||
use oauth2_types::{
|
||||
registration::ClientRegistrationResponse,
|
||||
requests::{AccessTokenResponse, ResponseMode},
|
||||
scope::{Scope, ScopeToken, OPENID},
|
||||
};
|
||||
use mas_data_model::{AccessToken, Client, TokenType, User};
|
||||
use mas_storage::{oauth2::OAuth2ClientRepository, RepositoryAccess};
|
||||
use oauth2_types::scope::{Scope, ScopeToken, OPENID};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::test_utils::{init_tracing, RequestBuilderExt, ResponseExt, TestState};
|
||||
|
||||
const GRAPHQL_SCOPE: ScopeToken = ScopeToken::from_static("urn:mas:graphql:*");
|
||||
async fn create_test_client(state: &TestState) -> Client {
|
||||
let mut repo = state.repository().await.unwrap();
|
||||
let mut rng = state.rng();
|
||||
|
||||
let client = repo
|
||||
.oauth2_client()
|
||||
.add(
|
||||
&mut rng,
|
||||
&state.clock,
|
||||
vec![],
|
||||
None,
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
repo.save().await.unwrap();
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
async fn create_test_user<U: Into<String> + Send>(state: &TestState, username: U) -> User {
|
||||
let username = username.into();
|
||||
let mut repo = state.repository().await.unwrap();
|
||||
let mut rng = state.rng();
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.add(&mut rng, &state.clock, username)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
repo.save().await.unwrap();
|
||||
|
||||
user
|
||||
}
|
||||
|
||||
async fn start_oauth_session(
|
||||
state: &TestState,
|
||||
client: &Client,
|
||||
user: &User,
|
||||
scope: Scope,
|
||||
) -> AccessToken {
|
||||
let mut repo = state.repository().await.unwrap();
|
||||
let mut rng = state.rng();
|
||||
|
||||
let browser_session = repo
|
||||
.browser_session()
|
||||
.add(&mut rng, &state.clock, user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.add(&mut rng, &state.clock, client, &browser_session, scope)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let access_token_str = TokenType::AccessToken.generate(&mut rng);
|
||||
|
||||
let access_token = repo
|
||||
.oauth2_access_token()
|
||||
.add(
|
||||
&mut rng,
|
||||
&state.clock,
|
||||
&session,
|
||||
access_token_str,
|
||||
Duration::minutes(5),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
repo.save().await.unwrap();
|
||||
|
||||
access_token
|
||||
}
|
||||
|
||||
const GRAPHQL: ScopeToken = ScopeToken::from_static("urn:mas:graphql:*");
|
||||
const ADMIN: ScopeToken = ScopeToken::from_static("urn:mas:admin");
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GraphQLResponse {
|
||||
#[serde(default)]
|
||||
data: serde_json::Value,
|
||||
errors: Option<Vec<serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
errors: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Test that the GraphQL endpoint can be queried with a GET request.
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_get(pool: PgPool) {
|
||||
init_tracing();
|
||||
let state = TestState::from_pool(pool).await.unwrap();
|
||||
|
||||
let request = Request::get("/graphql?query={viewer{__typename}}").empty();
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let response: GraphQLResponse = response.json();
|
||||
|
||||
assert!(response.errors.is_empty());
|
||||
assert_eq!(
|
||||
response.data,
|
||||
serde_json::json!({
|
||||
"viewer": {
|
||||
"__typename": "Anonymous",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the GraphQL endpoint can be queried with a POST request
|
||||
/// anonymously.
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_anonymous_viewer(pool: PgPool) {
|
||||
init_tracing();
|
||||
@@ -52,7 +167,7 @@ async fn test_anonymous_viewer(pool: PgPool) {
|
||||
response.assert_status(StatusCode::OK);
|
||||
let response: GraphQLResponse = response.json();
|
||||
|
||||
assert_eq!(response.errors, None);
|
||||
assert!(response.errors.is_empty());
|
||||
assert_eq!(
|
||||
response.data,
|
||||
serde_json::json!({
|
||||
@@ -63,110 +178,18 @@ async fn test_anonymous_viewer(pool: PgPool) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the GraphQL endpoint can be authenticated with a bearer token.
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_oauth2_viewer(pool: PgPool) {
|
||||
init_tracing();
|
||||
let state = TestState::from_pool(pool).await.unwrap();
|
||||
|
||||
// Start by creating a user, a client and a token
|
||||
// XXX: this is a lot of boilerplate just to get an access token!
|
||||
|
||||
// Provision a client
|
||||
let request =
|
||||
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
"response_types": ["code"],
|
||||
"grant_types": ["authorization_code"],
|
||||
}));
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::CREATED);
|
||||
|
||||
let ClientRegistrationResponse { client_id, .. } = response.json();
|
||||
|
||||
// Let's provision a user and create a session for them.
|
||||
let mut repo = state.repository().await.unwrap();
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.add(&mut state.rng(), &state.clock, "alice".to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let browser_session = repo
|
||||
.browser_session()
|
||||
.add(&mut state.rng(), &state.clock, &user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Lookup the client in the database.
|
||||
let client = repo
|
||||
.oauth2_client()
|
||||
.find_by_client_id(&client_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Start a grant
|
||||
let code = "thisisaverysecurecode";
|
||||
let grant = repo
|
||||
.oauth2_authorization_grant()
|
||||
.add(
|
||||
&mut state.rng(),
|
||||
&state.clock,
|
||||
&client,
|
||||
"https://example.com/redirect".parse().unwrap(),
|
||||
Scope::from_iter([OPENID, GRAPHQL_SCOPE]),
|
||||
Some(AuthorizationCode {
|
||||
code: code.to_owned(),
|
||||
pkce: None,
|
||||
}),
|
||||
Some("state".to_owned()),
|
||||
Some("nonce".to_owned()),
|
||||
None,
|
||||
ResponseMode::Query,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.add(
|
||||
&mut state.rng(),
|
||||
&state.clock,
|
||||
&client,
|
||||
&browser_session,
|
||||
grant.scope.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// And fulfill it
|
||||
let grant = repo
|
||||
.oauth2_authorization_grant()
|
||||
.fulfill(&state.clock, &session, grant)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
repo.save().await.unwrap();
|
||||
|
||||
// Now call the token endpoint to get an access token.
|
||||
let request = Request::post(mas_router::OAuth2TokenEndpoint::PATH).form(serde_json::json!({
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": grant.redirect_uri,
|
||||
"client_id": client.client_id,
|
||||
}));
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
|
||||
let AccessTokenResponse { access_token, .. } = response.json();
|
||||
let client = create_test_client(&state).await;
|
||||
let user = create_test_user(&state, "alice").await;
|
||||
let access_token =
|
||||
start_oauth_session(&state, &client, &user, Scope::from_iter([GRAPHQL])).await;
|
||||
let access_token = access_token.access_token;
|
||||
|
||||
let req = Request::post("/graphql")
|
||||
.bearer(&access_token)
|
||||
@@ -189,7 +212,7 @@ async fn test_oauth2_viewer(pool: PgPool) {
|
||||
response.assert_status(StatusCode::OK);
|
||||
let response: GraphQLResponse = response.json();
|
||||
|
||||
assert_eq!(response.errors, None);
|
||||
assert!(response.errors.is_empty());
|
||||
assert_eq!(
|
||||
response.data,
|
||||
serde_json::json!({
|
||||
@@ -201,3 +224,127 @@ async fn test_oauth2_viewer(pool: PgPool) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the GraphQL endpoint requires the GraphQL scope.
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_oauth2_no_scope(pool: PgPool) {
|
||||
init_tracing();
|
||||
let state = TestState::from_pool(pool).await.unwrap();
|
||||
|
||||
// Start by creating a user, a client and a token
|
||||
let client = create_test_client(&state).await;
|
||||
let user = create_test_user(&state, "alice").await;
|
||||
let access_token =
|
||||
start_oauth_session(&state, &client, &user, Scope::from_iter([OPENID])).await;
|
||||
let access_token = access_token.access_token;
|
||||
|
||||
let req = Request::post("/graphql")
|
||||
.bearer(&access_token)
|
||||
.json(serde_json::json!({
|
||||
"query": r#"
|
||||
query {
|
||||
viewer {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
"#,
|
||||
}));
|
||||
|
||||
let response = state.request(req).await;
|
||||
response.assert_status(StatusCode::UNAUTHORIZED);
|
||||
let response: GraphQLResponse = response.json();
|
||||
|
||||
assert_eq!(
|
||||
response.errors,
|
||||
vec![serde_json::json!({
|
||||
"message": "Missing urn:mas:graphql:* scope",
|
||||
})]
|
||||
);
|
||||
assert_eq!(response.data, serde_json::json!(null));
|
||||
}
|
||||
|
||||
/// Test the admin scope on the GraphQL endpoint.
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_oauth2_admin(pool: PgPool) {
|
||||
init_tracing();
|
||||
let state = TestState::from_pool(pool).await.unwrap();
|
||||
|
||||
// Start by creating a user, a client and two tokens
|
||||
let client = create_test_client(&state).await;
|
||||
let user = create_test_user(&state, "alice").await;
|
||||
|
||||
// Regular access token
|
||||
let access_token =
|
||||
start_oauth_session(&state, &client, &user, Scope::from_iter([GRAPHQL])).await;
|
||||
let access_token = access_token.access_token;
|
||||
|
||||
// Admin access token
|
||||
let access_token_admin =
|
||||
start_oauth_session(&state, &client, &user, Scope::from_iter([GRAPHQL, ADMIN])).await;
|
||||
let access_token_admin = access_token_admin.access_token;
|
||||
|
||||
// Create a second user and try to query stuff about it
|
||||
let user2 = create_test_user(&state, "bob").await;
|
||||
|
||||
let request = Request::post("/graphql")
|
||||
.bearer(&access_token)
|
||||
.json(serde_json::json!({
|
||||
"query": r#"
|
||||
query UserQuery($id: ID) {
|
||||
user(id: $id) {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
"#,
|
||||
"variables": {
|
||||
"id": format!("user:{id}", id = user2.id),
|
||||
},
|
||||
}));
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let response: GraphQLResponse = response.json();
|
||||
|
||||
// It should not find the user, because it's not the owner and not an admin
|
||||
assert!(response.errors.is_empty());
|
||||
assert_eq!(
|
||||
response.data,
|
||||
serde_json::json!({
|
||||
"user": null,
|
||||
})
|
||||
);
|
||||
|
||||
// Do the same request with the admin token
|
||||
let request = Request::post("/graphql")
|
||||
.bearer(&access_token_admin)
|
||||
.json(serde_json::json!({
|
||||
"query": r#"
|
||||
query UserQuery($id: ID) {
|
||||
user(id: $id) {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
"#,
|
||||
"variables": {
|
||||
"id": format!("user:{id}", id = user2.id),
|
||||
},
|
||||
}));
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
let response: GraphQLResponse = response.json();
|
||||
|
||||
// It should find the user, because the token has the admin scope
|
||||
assert!(response.errors.is_empty());
|
||||
assert_eq!(
|
||||
response.data,
|
||||
serde_json::json!({
|
||||
"user": {
|
||||
"id": format!("user:{id}", id = user2.id),
|
||||
"username": "bob",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user