Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot access security scheme field, because it's private #915

Open
BimaAdi opened this issue Nov 25, 2024 · 2 comments
Open

Cannot access security scheme field, because it's private #915

BimaAdi opened this issue Nov 25, 2024 · 2 comments
Labels
question Further information is requested

Comments

@BimaAdi
Copy link

BimaAdi commented Nov 25, 2024

I try openapi example on how to use auth-api key auth api openapi example.

use poem::{listener::TcpListener, Request, Route};
use poem_openapi::{auth::ApiKey, payload::PlainText, OpenApi, OpenApiService, SecurityScheme};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    pub username: String,
}

/// ApiKey authorization
#[derive(SecurityScheme)]
#[oai(
    ty = "api_key",
    key_name = "X-API-Key",
    key_in = "header",
    checker = "api_checker"
)]
pub struct MyApiKeyAuthorization(User);

pub async fn api_checker(_req: &Request, api_key: ApiKey) -> Option<User> {
    Some(User {
        username: api_key.key, // <- I simplified this column
    })
}

struct Api;

#[OpenApi]
#[allow(unused_variables)]
impl Api {
    /// This API returns the currently logged in user.
    #[oai(path = "/hello", method = "get")]
    async fn hello(&self, auth: MyApiKeyAuthorization) -> PlainText<String> {
        PlainText(auth.0.username) // <- return header key as plaintext
    }
}

#[tokio::main]
async fn main() {
    let api_service =
        OpenApiService::new(Api, "Authorization Demo", "1.0").server("http://localhost:3000/api");
    let ui = api_service.swagger_ui();
    let app = Route::new().nest("/api", api_service).nest("/", ui);

    poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(app)
        .await
        .unwrap()
}

It's work like a charm no error. Then I try to move authorization struct to different module/file like this:

use poem::{listener::TcpListener, Route};
use poem_openapi::{payload::PlainText, OpenApi, OpenApiService};
use shared::MyApiKeyAuthorization;

pub mod shared {
    use poem::Request;
    use poem_openapi::{auth::ApiKey, SecurityScheme};
    use serde::{Deserialize, Serialize};
    #[derive(Debug, Serialize, Deserialize)]
    pub struct User {
        pub username: String,
    }

    /// ApiKey authorization
    #[derive(SecurityScheme)]
    #[oai(
        ty = "api_key",
        key_name = "X-API-Key",
        key_in = "header",
        checker = "api_checker"
    )]
    pub struct MyApiKeyAuthorization(User);

    pub async fn api_checker(_req: &Request, api_key: ApiKey) -> Option<User> {
        Some(User {
            username: api_key.key,
        })
    }
}

struct Api;

#[OpenApi]
#[allow(unused_variables)]
impl Api {
    /// This API returns the currently logged in user.
    #[oai(path = "/hello", method = "get")]
    async fn hello(&self, auth: MyApiKeyAuthorization) -> PlainText<String> {
        PlainText(auth.0.username)
    }
}

#[tokio::main]
async fn main() {
    let api_service =
        OpenApiService::new(Api, "Authorization Demo", "1.0").server("http://localhost:3000/api");
    let ui = api_service.swagger_ui();
    let app = Route::new().nest("/api", api_service).nest("/", ui);

    poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(app)
        .await
        .unwrap()
}

then I got this error

error[E0616]: field `0` of struct `MyApiKeyAuthorization` is private
  --> src/bin/main_error.rs:39:24
   |
39 |         PlainText(auth.0.username)
   |                        ^ private field

For more information about this error, try `rustc --explain E0616`.
error: could not compile `bug-poem-auth` (bin "main_error") due to 1 previous error

It's say it's private field but I already set struct and it's field as public. How to move Authorization struct to different module/file without error and access it's field value?

I use rust version rustc 1.82.0 (f6e511eec 2024-10-15) and here my depedencies

[dependencies]
poem = "3"
poem-openapi = { version = "5", features = ["swagger-ui"]}
serde = "1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
@BimaAdi BimaAdi added the question Further information is requested label Nov 25, 2024
@BimaAdi
Copy link
Author

BimaAdi commented Nov 27, 2024

Why Security Scheme must be public?

Let's say we work on big project with many route. In order to make maintaining code easier and reduce git conflict we split route in to several module/file like this:

use bar_route::BarRoute;
use foo_route::FooRoute;
use poem::{listener::TcpListener, Route};
use poem_openapi::OpenApiService;

mod foo_route {
    use poem_openapi::{payload::PlainText, OpenApi};

    pub struct FooRoute;

    #[OpenApi]
    impl FooRoute {
        #[oai(path = "/foo", method = "get")]
        async fn foo(&self) -> PlainText<String> {
            PlainText("foo".to_string())
        }
    }
}

mod bar_route {
    use poem_openapi::{payload::PlainText, OpenApi};

    pub struct BarRoute;

    #[OpenApi]
    impl BarRoute {
        #[oai(path = "/bar", method = "get")]
        async fn foo(&self) -> PlainText<String> {
            PlainText("bar".to_string())
        }
    }
}

#[tokio::main]
async fn main() {
    let api_service = OpenApiService::new((FooRoute, BarRoute), "Authorization Demo", "1.0")
        .server("http://localhost:3000/api");
    let ui = api_service.swagger_ui();
    let app = Route::new().nest("/api", api_service).nest("/", ui);

    poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(app)
        .await
        .unwrap()
}

In above example i use module foo and bar but In real world it can be module user, post etc. Although they are on different module/file but they use same authorization. So we add authorization like this:

use bar_route::BarRoute;
use foo_route::FooRoute;
use poem::{listener::TcpListener, Route};
use poem_openapi::OpenApiService;

pub mod shared {
    use poem::Request;
    use poem_openapi::{auth::ApiKey, SecurityScheme};
    use serde::{Deserialize, Serialize};
    #[derive(Debug, Serialize, Deserialize)]
    pub struct User {
        pub username: String,
    }

    /// ApiKey authorization
    #[derive(SecurityScheme)]
    #[oai(
        ty = "api_key",
        key_name = "X-API-Key",
        key_in = "header",
        checker = "api_checker"
    )]
    pub struct MyApiKeyAuthorization(User);

    pub async fn api_checker(_req: &Request, api_key: ApiKey) -> Option<User> {
        Some(User {
            username: api_key.key,
        })
    }
}

mod foo_route {
    use poem_openapi::{payload::PlainText, OpenApi};

    use crate::shared::MyApiKeyAuthorization;

    pub struct FooRoute;

    #[OpenApi]
    impl FooRoute {
        #[oai(path = "/foo", method = "get")]
        async fn foo(&self, auth: MyApiKeyAuthorization) -> PlainText<String> {
            println!("{:?}", auth.0.username); // Error due private field
            PlainText("foo".to_string())
        }
    }
}

mod bar_route {
    use poem_openapi::{payload::PlainText, OpenApi};

    use crate::shared::MyApiKeyAuthorization;

    pub struct BarRoute;

    #[OpenApi]
    impl BarRoute {
        #[oai(path = "/bar", method = "get")]
        async fn foo(&self, auth: MyApiKeyAuthorization) -> PlainText<String> {
            println!("{:?}", auth.0.username); // Error due private field
            PlainText("bar".to_string())
        }
    }
}

#[tokio::main]
async fn main() {
    let api_service = OpenApiService::new((FooRoute, BarRoute), "Authorization Demo", "1.0")
        .server("http://localhost:3000/api");
    let ui = api_service.swagger_ui();
    let app = Route::new().nest("/api", api_service).nest("/", ui);

    poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(app)
        .await
        .unwrap()
}

Code above will failed to compile due private field. It would be nice if we can put SecurityScheme in different file/module and use it in any file/module.

@BimaAdi
Copy link
Author

BimaAdi commented Nov 27, 2024

Workaround that I found

I try to find workaround. I inspect openapi schema generated by poem openapi. It's turn out, it set securitySchemes name using struct name

"securitySchemes": {
      "MyApiKeyAuthorization": {
        "type": "apiKey",
        "description": "ApiKey authorization",
        "name": "X-API-Key",
        "in": "header"
      }
    }

So by define SecuritySchema using struct with the same name, Openapi will registered it as same authorization, Although we define it more than once. Here my full solution:

use bar_route::BarRoute;
use foo_route::FooRoute;
use poem::{listener::TcpListener, Route};
use poem_openapi::OpenApiService;

mod foo_route {
    use poem::Request;
    use poem_openapi::{auth::ApiKey, payload::PlainText, OpenApi, SecurityScheme};
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Serialize, Deserialize)]
    pub struct User {
        pub username: String,
    }

    /// ApiKey authorization
    #[derive(SecurityScheme)]
    #[oai(
        ty = "api_key",
        key_name = "X-API-Key",
        key_in = "header",
        checker = "api_checker"
    )]
    pub struct MyApiKeyAuthorization(User); // <- Make sure it's has same name with bar_route SecurityScheme

    pub async fn api_checker(_req: &Request, api_key: ApiKey) -> Option<User> {
        Some(User {
            username: api_key.key,
        })
    }

    pub struct FooRoute;

    #[OpenApi]
    impl FooRoute {
        #[oai(path = "/foo", method = "get")]
        async fn foo(&self, auth: MyApiKeyAuthorization) -> PlainText<String> {
            println!("{:?}", auth.0.username);
            PlainText("foo".to_string())
        }
    }
}

mod bar_route {
    use poem::Request;
    use poem_openapi::{auth::ApiKey, payload::PlainText, OpenApi, SecurityScheme};
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Serialize, Deserialize)]
    pub struct User {
        pub username: String,
    }

    /// ApiKey authorization
    #[derive(SecurityScheme)]
    #[oai(
        ty = "api_key",
        key_name = "X-API-Key",
        key_in = "header",
        checker = "api_checker"
    )]
    pub struct MyApiKeyAuthorization(User); // <- Make sure it's has same name with foo_route SecurityScheme

    pub async fn api_checker(_req: &Request, api_key: ApiKey) -> Option<User> {
        Some(User {
            username: api_key.key,
        })
    }

    pub struct BarRoute;

    #[OpenApi]
    impl BarRoute {
        #[oai(path = "/bar", method = "get")]
        async fn foo(&self, auth: MyApiKeyAuthorization) -> PlainText<String> {
            println!("{:?}", auth.0.username);
            PlainText("bar".to_string())
        }
    }
}

#[tokio::main]
async fn main() {
    let api_service = OpenApiService::new((FooRoute, BarRoute), "Authorization Demo", "1.0")
        .server("http://localhost:3000/api");
    let ui = api_service.swagger_ui();
    let app = Route::new().nest("/api", api_service).nest("/", ui);

    poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(app)
        .await
        .unwrap()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

1 participant