diff options
| -rw-r--r-- | fatcat-openapi2.yml | 26 | ||||
| -rw-r--r-- | rust/HACKING.md | 9 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/README.md | 3 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/api.yaml | 26 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/api/swagger.yaml | 66 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/examples/client.rs | 9 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/examples/server_lib/server.rs | 8 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/src/client.rs | 103 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/src/lib.rs | 22 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/src/mimetypes.rs | 20 | ||||
| -rw-r--r-- | rust/fatcat-api-spec/src/server.rs | 118 | ||||
| -rw-r--r-- | rust/src/api_wrappers.rs | 96 | ||||
| -rw-r--r-- | rust/src/auth.rs | 21 | 
13 files changed, 480 insertions, 47 deletions
| diff --git a/fatcat-openapi2.yml b/fatcat-openapi2.yml index 501a1296..625a0143 100644 --- a/fatcat-openapi2.yml +++ b/fatcat-openapi2.yml @@ -2711,4 +2711,30 @@ paths:            schema:              $ref: "#/definitions/error_response"          <<: *AUTHRESPONSES +  /auth/check: +    get: +      operationId: "auth_check" +      tags: # TAGLINE +      security: +        # required admin privs +        - Bearer: [] +      parameters: +        - name: role +          in: query +          required: false +          type: string +      responses: +        200: +          description: Success +          schema: +            $ref: "#/definitions/success" +        400: +          description: Bad Request +          schema: +            $ref: "#/definitions/error_response" +        500: +          description: Generic Error +          schema: +            $ref: "#/definitions/error_response" +        <<: *AUTHRESPONSES diff --git a/rust/HACKING.md b/rust/HACKING.md index 9d161b87..b3a551fa 100644 --- a/rust/HACKING.md +++ b/rust/HACKING.md @@ -54,6 +54,15 @@ Debug SQL schema errors (if diesel commands fail):  ## Direct API Interaction +First setup an auth token and check that authentication is working + +    EDITOR_ID='aaaaaaaaaaaabkvkaaaaaaaaay' +    AUTH_TOKEN=`./target/debug/fatcat-auth create-token $EDITOR_ID` +    http get :9411/v0/auth/check "Authorization:Bearer $AUTH_TOKEN" +    http get :9411/v0/auth/check?role=admin "Authorization:Bearer $AUTH_TOKEN" + +You'll need to add the `$AUTH_TOKEN` bit to all requests below. +      Creating entities via API:      http --json post localhost:9411/v0/container name=asdf issn=1234-5678 diff --git a/rust/fatcat-api-spec/README.md b/rust/fatcat-api-spec/README.md index f81f641a..f8a6e817 100644 --- a/rust/fatcat-api-spec/README.md +++ b/rust/fatcat-api-spec/README.md @@ -13,7 +13,7 @@ To see how to make this your own, look here:  [README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md)  - API version: 0.1.0 -- Build date: 2019-01-04T05:57:03.701Z +- Build date: 2019-01-08T01:49:55.777Z  This autogenerated project defines an API crate `fatcat` which contains:  * An `Api` trait defining the API in Rust. @@ -79,6 +79,7 @@ cargo run --example client GetCreatorReleases  cargo run --example client GetCreatorRevision  cargo run --example client LookupCreator  cargo run --example client UpdateCreator +cargo run --example client AuthCheck  cargo run --example client AuthOidc  cargo run --example client GetEditor  cargo run --example client GetEditorChangelog diff --git a/rust/fatcat-api-spec/api.yaml b/rust/fatcat-api-spec/api.yaml index 501a1296..625a0143 100644 --- a/rust/fatcat-api-spec/api.yaml +++ b/rust/fatcat-api-spec/api.yaml @@ -2711,4 +2711,30 @@ paths:            schema:              $ref: "#/definitions/error_response"          <<: *AUTHRESPONSES +  /auth/check: +    get: +      operationId: "auth_check" +      tags: # TAGLINE +      security: +        # required admin privs +        - Bearer: [] +      parameters: +        - name: role +          in: query +          required: false +          type: string +      responses: +        200: +          description: Success +          schema: +            $ref: "#/definitions/success" +        400: +          description: Bad Request +          schema: +            $ref: "#/definitions/error_response" +        500: +          description: Generic Error +          schema: +            $ref: "#/definitions/error_response" +        <<: *AUTHRESPONSES diff --git a/rust/fatcat-api-spec/api/swagger.yaml b/rust/fatcat-api-spec/api/swagger.yaml index a19d6ae1..9d4767c0 100644 --- a/rust/fatcat-api-spec/api/swagger.yaml +++ b/rust/fatcat-api-spec/api/swagger.yaml @@ -6659,6 +6659,72 @@ paths:        HttpMethod: "Post"        httpmethod: "post"        noClientExample: true +  /auth/check: +    get: +      operationId: "auth_check" +      parameters: +      - name: "role" +        in: "query" +        required: false +        type: "string" +        formatString: "{:?}" +        example: "Some(\"role_example\".to_string())" +      responses: +        200: +          description: "Success" +          schema: +            $ref: "#/definitions/success" +          x-responseId: "Success" +          x-uppercaseResponseId: "SUCCESS" +          uppercase_operation_id: "AUTH_CHECK" +          uppercase_data_type: "SUCCESS" +          producesJson: true +        400: +          description: "Bad Request" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "BadRequest" +          x-uppercaseResponseId: "BAD_REQUEST" +          uppercase_operation_id: "AUTH_CHECK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        401: +          description: "Not Authorized" +          schema: +            $ref: "#/definitions/error_response" +          headers: +            WWW_Authenticate: +              type: "string" +          x-responseId: "NotAuthorized" +          x-uppercaseResponseId: "NOT_AUTHORIZED" +          uppercase_operation_id: "AUTH_CHECK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        403: +          description: "Forbidden" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "Forbidden" +          x-uppercaseResponseId: "FORBIDDEN" +          uppercase_operation_id: "AUTH_CHECK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +        500: +          description: "Generic Error" +          schema: +            $ref: "#/definitions/error_response" +          x-responseId: "GenericError" +          x-uppercaseResponseId: "GENERIC_ERROR" +          uppercase_operation_id: "AUTH_CHECK" +          uppercase_data_type: "ERRORRESPONSE" +          producesJson: true +      security: +      - Bearer: [] +      operation_id: "auth_check" +      uppercase_operation_id: "AUTH_CHECK" +      path: "/auth/check" +      HttpMethod: "Get" +      httpmethod: "get"  securityDefinitions:    Bearer:      type: "apiKey" diff --git a/rust/fatcat-api-spec/examples/client.rs b/rust/fatcat-api-spec/examples/client.rs index 4eed8ae4..5a43a33c 100644 --- a/rust/fatcat-api-spec/examples/client.rs +++ b/rust/fatcat-api-spec/examples/client.rs @@ -12,8 +12,8 @@ extern crate uuid;  use clap::{App, Arg};  #[allow(unused_imports)]  use fatcat::{ -    AcceptEditgroupResponse, ApiError, ApiNoContext, AuthOidcResponse, ContextWrapperExt, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, -    CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, +    AcceptEditgroupResponse, ApiError, ApiNoContext, AuthCheckResponse, AuthOidcResponse, ContextWrapperExt, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, +    CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse,      CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse,      DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse,      DeleteWebcaptureEditResponse, DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, @@ -54,6 +54,7 @@ fn main() {                      "GetCreatorReleases",                      "GetCreatorRevision",                      "LookupCreator", +                    "AuthCheck",                      "GetEditor",                      "GetEditorChangelog",                      "AcceptEditgroup", @@ -272,6 +273,10 @@ fn main() {          //     let result = client.update_creator("ident_example".to_string(), ???, Some("editgroup_id_example".to_string())).wait();          //     println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>")));          //  }, +        Some("AuthCheck") => { +            let result = client.auth_check(Some("role_example".to_string())).wait(); +            println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from("<none>"))); +        }          // Disabled because there's no example.          // Some("AuthOidc") => { diff --git a/rust/fatcat-api-spec/examples/server_lib/server.rs b/rust/fatcat-api-spec/examples/server_lib/server.rs index 5e86a10e..73917351 100644 --- a/rust/fatcat-api-spec/examples/server_lib/server.rs +++ b/rust/fatcat-api-spec/examples/server_lib/server.rs @@ -11,7 +11,7 @@ use swagger;  use fatcat::models;  use fatcat::{ -    AcceptEditgroupResponse, Api, ApiError, AuthOidcResponse, Context, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, +    AcceptEditgroupResponse, Api, ApiError, AuthCheckResponse, AuthOidcResponse, Context, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse,      CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse,      CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse,      DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, @@ -297,6 +297,12 @@ impl Api for Server {          Box::new(futures::failed("Generic failure".into()))      } +    fn auth_check(&self, role: Option<String>, context: &Context) -> Box<Future<Item = AuthCheckResponse, Error = ApiError> + Send> { +        let context = context.clone(); +        println!("auth_check({:?}) - X-Span-ID: {:?}", role, context.x_span_id.unwrap_or(String::from("<none>")).clone()); +        Box::new(futures::failed("Generic failure".into())) +    } +      fn auth_oidc(&self, oidc_params: models::AuthOidc, context: &Context) -> Box<Future<Item = AuthOidcResponse, Error = ApiError> + Send> {          let context = context.clone();          println!("auth_oidc({:?}) - X-Span-ID: {:?}", oidc_params, context.x_span_id.unwrap_or(String::from("<none>")).clone()); diff --git a/rust/fatcat-api-spec/src/client.rs b/rust/fatcat-api-spec/src/client.rs index 470a5350..7f364eb4 100644 --- a/rust/fatcat-api-spec/src/client.rs +++ b/rust/fatcat-api-spec/src/client.rs @@ -35,18 +35,19 @@ use swagger::{ApiError, Context, XSpanId};  use models;  use { -    AcceptEditgroupResponse, Api, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, -    CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWebcaptureBatchResponse, -    CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, DeleteCreatorResponse, -    DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, DeleteWebcaptureEditResponse, -    DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, GetContainerHistoryResponse, -    GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, GetCreatorReleasesResponse, -    GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, GetFileRedirectsResponse, -    GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, GetReleaseEditResponse, -    GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, GetReleaseWebcapturesResponse, -    GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, GetWorkHistoryResponse, -    GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, -    UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, UpdateWorkResponse, +    AcceptEditgroupResponse, Api, AuthCheckResponse, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, +    CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, +    CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, +    DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, +    DeleteWebcaptureEditResponse, DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, +    GetContainerHistoryResponse, GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, +    GetCreatorReleasesResponse, GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, +    GetFileRedirectsResponse, GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, +    GetReleaseEditResponse, GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, +    GetReleaseWebcapturesResponse, GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, +    GetWorkHistoryResponse, GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, +    LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, +    UpdateWorkResponse,  };  /// Convert input into a base path, e.g. "http://example:123". Also checks the scheme as it goes. @@ -2030,6 +2031,84 @@ impl Api for Client {          Box::new(futures::done(result))      } +    fn auth_check(&self, param_role: Option<String>, context: &Context) -> Box<Future<Item = AuthCheckResponse, Error = ApiError> + Send> { +        // Query parameters +        let query_role = param_role.map_or_else(String::new, |query| format!("role={role}&", role = query.to_string())); + +        let url = format!("{}/v0/auth/check?{role}", self.base_path, role = utf8_percent_encode(&query_role, QUERY_ENCODE_SET)); + +        let hyper_client = (self.hyper_client)(); +        let request = hyper_client.request(hyper::method::Method::Get, &url); +        let mut custom_headers = hyper::header::Headers::new(); + +        context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + +        let request = request.headers(custom_headers); + +        // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +        fn parse_response(mut response: hyper::client::response::Response) -> Result<AuthCheckResponse, ApiError> { +            match response.status.to_u16() { +                200 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::Success>(&buf)?; + +                    Ok(AuthCheckResponse::Success(body)) +                } +                400 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(AuthCheckResponse::BadRequest(body)) +                } +                401 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; +                    header! { (ResponseWwwAuthenticate, "WWW_Authenticate") => [String] } +                    let response_www_authenticate = response +                        .headers +                        .get::<ResponseWwwAuthenticate>() +                        .ok_or_else(|| "Required response header WWW_Authenticate for response 401 was not found.")?; + +                    Ok(AuthCheckResponse::NotAuthorized { +                        body: body, +                        www_authenticate: response_www_authenticate.0.clone(), +                    }) +                } +                403 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(AuthCheckResponse::Forbidden(body)) +                } +                500 => { +                    let mut buf = String::new(); +                    response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; +                    let body = serde_json::from_str::<models::ErrorResponse>(&buf)?; + +                    Ok(AuthCheckResponse::GenericError(body)) +                } +                code => { +                    let mut buf = [0; 100]; +                    let debug_body = match response.read(&mut buf) { +                        Ok(len) => match str::from_utf8(&buf[..len]) { +                            Ok(body) => Cow::from(body), +                            Err(_) => Cow::from(format!("<Body was not UTF8: {:?}>", &buf[..len].to_vec())), +                        }, +                        Err(e) => Cow::from(format!("<Failed to read body: {}>", e)), +                    }; +                    Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) +                } +            } +        } + +        let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); +        Box::new(futures::done(result)) +    } +      fn auth_oidc(&self, param_oidc_params: models::AuthOidc, context: &Context) -> Box<Future<Item = AuthOidcResponse, Error = ApiError> + Send> {          let url = format!("{}/v0/auth/oidc", self.base_path); diff --git a/rust/fatcat-api-spec/src/lib.rs b/rust/fatcat-api-spec/src/lib.rs index 258b635b..17c74384 100644 --- a/rust/fatcat-api-spec/src/lib.rs +++ b/rust/fatcat-api-spec/src/lib.rs @@ -349,6 +349,20 @@ pub enum UpdateCreatorResponse {  }  #[derive(Debug, PartialEq)] +pub enum AuthCheckResponse { +    /// Success +    Success(models::Success), +    /// Bad Request +    BadRequest(models::ErrorResponse), +    /// Not Authorized +    NotAuthorized { body: models::ErrorResponse, www_authenticate: String }, +    /// Forbidden +    Forbidden(models::ErrorResponse), +    /// Generic Error +    GenericError(models::ErrorResponse), +} + +#[derive(Debug, PartialEq)]  pub enum AuthOidcResponse {      /// Found      Found(models::AuthOidcResult), @@ -1315,6 +1329,8 @@ pub trait Api {      fn update_creator(&self, ident: String, entity: models::CreatorEntity, editgroup_id: Option<String>, context: &Context) -> Box<Future<Item = UpdateCreatorResponse, Error = ApiError> + Send>; +    fn auth_check(&self, role: Option<String>, context: &Context) -> Box<Future<Item = AuthCheckResponse, Error = ApiError> + Send>; +      fn auth_oidc(&self, oidc_params: models::AuthOidc, context: &Context) -> Box<Future<Item = AuthOidcResponse, Error = ApiError> + Send>;      fn get_editor(&self, editor_id: String, context: &Context) -> Box<Future<Item = GetEditorResponse, Error = ApiError> + Send>; @@ -1565,6 +1581,8 @@ pub trait ApiNoContext {      fn update_creator(&self, ident: String, entity: models::CreatorEntity, editgroup_id: Option<String>) -> Box<Future<Item = UpdateCreatorResponse, Error = ApiError> + Send>; +    fn auth_check(&self, role: Option<String>) -> Box<Future<Item = AuthCheckResponse, Error = ApiError> + Send>; +      fn auth_oidc(&self, oidc_params: models::AuthOidc) -> Box<Future<Item = AuthOidcResponse, Error = ApiError> + Send>;      fn get_editor(&self, editor_id: String) -> Box<Future<Item = GetEditorResponse, Error = ApiError> + Send>; @@ -1852,6 +1870,10 @@ impl<'a, T: Api> ApiNoContext for ContextWrapper<'a, T> {          self.api().update_creator(ident, entity, editgroup_id, &self.context())      } +    fn auth_check(&self, role: Option<String>) -> Box<Future<Item = AuthCheckResponse, Error = ApiError> + Send> { +        self.api().auth_check(role, &self.context()) +    } +      fn auth_oidc(&self, oidc_params: models::AuthOidc) -> Box<Future<Item = AuthOidcResponse, Error = ApiError> + Send> {          self.api().auth_oidc(oidc_params, &self.context())      } diff --git a/rust/fatcat-api-spec/src/mimetypes.rs b/rust/fatcat-api-spec/src/mimetypes.rs index cfdd357d..83add9e3 100644 --- a/rust/fatcat-api-spec/src/mimetypes.rs +++ b/rust/fatcat-api-spec/src/mimetypes.rs @@ -452,6 +452,26 @@ pub mod responses {      lazy_static! {          pub static ref UPDATE_CREATOR_GENERIC_ERROR: Mime = mime!(Application / Json);      } +    /// Create Mime objects for the response content types for AuthCheck +    lazy_static! { +        pub static ref AUTH_CHECK_SUCCESS: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for AuthCheck +    lazy_static! { +        pub static ref AUTH_CHECK_BAD_REQUEST: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for AuthCheck +    lazy_static! { +        pub static ref AUTH_CHECK_NOT_AUTHORIZED: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for AuthCheck +    lazy_static! { +        pub static ref AUTH_CHECK_FORBIDDEN: Mime = mime!(Application / Json); +    } +    /// Create Mime objects for the response content types for AuthCheck +    lazy_static! { +        pub static ref AUTH_CHECK_GENERIC_ERROR: Mime = mime!(Application / Json); +    }      /// Create Mime objects for the response content types for AuthOidc      lazy_static! {          pub static ref AUTH_OIDC_FOUND: Mime = mime!(Application / Json); diff --git a/rust/fatcat-api-spec/src/server.rs b/rust/fatcat-api-spec/src/server.rs index c0903676..d8fc7dc2 100644 --- a/rust/fatcat-api-spec/src/server.rs +++ b/rust/fatcat-api-spec/src/server.rs @@ -37,18 +37,19 @@ use swagger::{ApiError, Context, XSpanId};  #[allow(unused_imports)]  use models;  use { -    AcceptEditgroupResponse, Api, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, -    CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWebcaptureBatchResponse, -    CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, DeleteCreatorResponse, -    DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, DeleteWebcaptureEditResponse, -    DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, GetContainerHistoryResponse, -    GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, GetCreatorReleasesResponse, -    GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, GetFileRedirectsResponse, -    GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, GetReleaseEditResponse, -    GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, GetReleaseWebcapturesResponse, -    GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, GetWorkHistoryResponse, -    GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, -    UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, UpdateWorkResponse, +    AcceptEditgroupResponse, Api, AuthCheckResponse, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, +    CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, +    CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, +    DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, +    DeleteWebcaptureEditResponse, DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, +    GetContainerHistoryResponse, GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, +    GetCreatorReleasesResponse, GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, +    GetFileRedirectsResponse, GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, +    GetReleaseEditResponse, GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, +    GetReleaseWebcapturesResponse, GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, +    GetWorkHistoryResponse, GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, +    LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, +    UpdateWorkResponse,  };  header! { (Warning, "Warning") => [String] } @@ -2606,6 +2607,99 @@ where      );      let api_clone = api.clone(); +    router.get( +        "/v0/auth/check", +        move |req: &mut Request| { +            let mut context = Context::default(); + +            // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). +            fn handle_request<T>(req: &mut Request, api: &T, context: &mut Context) -> Result<Response, Response> +            where +                T: Api, +            { +                context.x_span_id = Some(req.headers.get::<XSpanId>().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); +                context.auth_data = req.extensions.remove::<AuthData>(); +                context.authorization = req.extensions.remove::<Authorization>(); + +                let authorization = context.authorization.as_ref().ok_or_else(|| Response::with((status::Forbidden, "Unauthenticated".to_string())))?; + +                // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) +                let query_params = req.get::<UrlEncodedQuery>().unwrap_or_default(); +                let param_role = query_params.get("role").and_then(|list| list.first()).and_then(|x| x.parse::<String>().ok()); + +                match api.auth_check(param_role, context).wait() { +                    Ok(rsp) => match rsp { +                        AuthCheckResponse::Success(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(200), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_SUCCESS.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        AuthCheckResponse::BadRequest(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(400), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_BAD_REQUEST.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        AuthCheckResponse::NotAuthorized { body, www_authenticate } => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(401), body_string)); +                            header! { (ResponseWwwAuthenticate, "WWW_Authenticate") => [String] } +                            response.headers.set(ResponseWwwAuthenticate(www_authenticate)); + +                            response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_NOT_AUTHORIZED.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        AuthCheckResponse::Forbidden(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(403), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_FORBIDDEN.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                        AuthCheckResponse::GenericError(body) => { +                            let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + +                            let mut response = Response::with((status::Status::from_u16(500), body_string)); +                            response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_GENERIC_ERROR.clone())); + +                            context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + +                            Ok(response) +                        } +                    }, +                    Err(_) => { +                        // Application code returned an error. This should not happen, as the implementation should +                        // return a valid response. +                        Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) +                    } +                } +            } + +            handle_request(req, &api_clone, &mut context).or_else(|mut response| { +                context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); +                Ok(response) +            }) +        }, +        "AuthCheck", +    ); + +    let api_clone = api.clone();      router.post(          "/v0/auth/oidc",          move |req: &mut Request| { diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 614a0007..818a41c2 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -404,8 +404,7 @@ macro_rules! wrap_entity_handlers {                  $model::db_delete_edit(&conn, edit_id)              }) {                  Ok(()) => -                    $delete_edit_resp::DeletedEdit(Success { message: format!("Successfully deleted work-in-progress {} edit: {}", stringify!($model), edit_id) } ), -                Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => +                    $delete_edit_resp::DeletedEdit(Success { message: format!("Successfully deleted work-in-progress {} edit: {}", stringify!($model), edit_id) } ), Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) =>                      $delete_edit_resp::NotFound(ErrorResponse { message: format!("No such {} edit: {}", stringify!($model), edit_id) }),                  Err(Error(ErrorKind::Diesel(e), _)) =>                      $delete_edit_resp::BadRequest(ErrorResponse { message: e.to_string() }), @@ -918,9 +917,11 @@ impl Api for Server {                      ErrorKind::OtherBadRequest("editor_id doesn't match".to_string()).into(),                  );              } -            let auth_context = self -                .auth_confectionary -                .require_auth(&conn, &context.auth_data, Some("update_editor"))?; +            let auth_context = self.auth_confectionary.require_auth( +                &conn, +                &context.auth_data, +                Some("update_editor"), +            )?;              let editor_id = FatCatId::from_str(&editor_id)?;              // DANGER! these permissions are for username updates only!              if editor_id == auth_context.editor_id { @@ -986,9 +987,11 @@ impl Api for Server {          let conn = self.db_pool.get().expect("db_pool error");          let ret = match conn.transaction(|| {              let editgroup_id = FatCatId::from_str(&editgroup_id)?; -            let auth_context = self -                .auth_confectionary -                .require_auth(&conn, &context.auth_data, Some("accept_editgroup"))?; +            let auth_context = self.auth_confectionary.require_auth( +                &conn, +                &context.auth_data, +                Some("accept_editgroup"), +            )?;              auth_context.require_role(FatcatRole::Admin)?;              // NOTE: this is currently redundant, but zero-cost              auth_context.require_editgroup(&conn, editgroup_id)?; @@ -1058,9 +1061,11 @@ impl Api for Server {      ) -> Box<Future<Item = CreateEditgroupResponse, Error = ApiError> + Send> {          let conn = self.db_pool.get().expect("db_pool error");          let ret = match conn.transaction(|| { -            let auth_context = self -                .auth_confectionary -                .require_auth(&conn, &context.auth_data, Some("create_editgroup"))?; +            let auth_context = self.auth_confectionary.require_auth( +                &conn, +                &context.auth_data, +                Some("create_editgroup"), +            )?;              auth_context.require_role(FatcatRole::Editor)?;              let mut entity = entity.clone();              match entity.editor_id.clone() { @@ -1149,9 +1154,11 @@ impl Api for Server {      ) -> Box<Future<Item = AuthOidcResponse, Error = ApiError> + Send> {          let conn = self.db_pool.get().expect("db_pool error");          let ret = match conn.transaction(|| { -            let auth_context = self -                .auth_confectionary -                .require_auth(&conn, &context.auth_data, Some("auth_oidc"))?; +            let auth_context = self.auth_confectionary.require_auth( +                &conn, +                &context.auth_data, +                Some("auth_oidc"), +            )?;              auth_context.require_role(FatcatRole::Superuser)?;              let (editor, created) = self.auth_oidc_handler(params, &conn)?;              // create an auth token; leave it to webface to attenuate to a given duration @@ -1221,4 +1228,65 @@ impl Api for Server {          };          Box::new(futures::done(Ok(ret)))      } + +    fn auth_check( +        &self, +        role: Option<String>, +        context: &Context, +    ) -> Box<Future<Item = AuthCheckResponse, Error = ApiError> + Send> { +        let conn = self.db_pool.get().expect("db_pool error"); +        let ret = match conn.transaction(|| { +            let auth_context = self.auth_confectionary.require_auth( +                &conn, +                &context.auth_data, +                Some("auth_check"), +            )?; +            if let Some(role) = role { +                let role = match role.to_lowercase().as_ref() { +                    "superuser" => FatcatRole::Superuser, +                    "admin" => FatcatRole::Admin, +                    "editor" => FatcatRole::Editor, +                    "bot" => FatcatRole::Bot, +                    "human" => FatcatRole::Human, +                    "public" => FatcatRole::Public, +                    _ => bail!("unknown auth role: {}", role), +                }; +                auth_context.require_role(role)?; +            }; +            Ok(()) +        }) { +            Ok(()) => AuthCheckResponse::Success(Success { +                message: "auth check successful!".to_string() }), +            Err(Error(ErrorKind::Diesel(e), _)) => AuthCheckResponse::BadRequest(ErrorResponse { +                message: e.to_string(), +            }), +            Err(Error(ErrorKind::Uuid(e), _)) => AuthCheckResponse::BadRequest(ErrorResponse { +                message: e.to_string(), +            }), +            Err(Error(ErrorKind::InvalidCredentials(e), _)) => +            // TODO: why can't I NotAuthorized here? +            { +                AuthCheckResponse::Forbidden(ErrorResponse { +                    message: e.to_string(), +                }) +            }, +            Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { +                AuthCheckResponse::Forbidden(ErrorResponse { +                    message: e.to_string(), +                }) +            }, +            Err(Error(ErrorKind::OtherBadRequest(e), _)) => { +                AuthCheckResponse::BadRequest(ErrorResponse { +                    message: e.to_string(), +                }) +            }, +            Err(e) => { +                error!("{}", e); +                AuthCheckResponse::GenericError(ErrorResponse { +                    message: e.to_string(), +                }) +            }, +        }; +        Box::new(futures::done(Ok(ret))) +    }  } diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 8894e33b..c20b9b71 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -56,9 +56,10 @@ impl AuthContext {      pub fn require_role(&self, role: FatcatRole) -> Result<()> {          match self.has_role(role) {              true => Ok(()), -            false => Err(ErrorKind::InsufficientPrivileges( -                format!("doesn't have required role: {:?}", role), -            ) +            false => Err(ErrorKind::InsufficientPrivileges(format!( +                "doesn't have required role: {:?}", +                role +            ))              .into()),          }      } @@ -230,7 +231,12 @@ impl AuthConfectionary {          Ok(BASE64.encode(&raw))      } -    pub fn parse_macaroon_token(&self, conn: &DbConn, s: &str, endpoint: Option<&str>) -> Result<EditorRow> { +    pub fn parse_macaroon_token( +        &self, +        conn: &DbConn, +        s: &str, +        endpoint: Option<&str>, +    ) -> Result<EditorRow> {          let raw = BASE64.decode(s.as_bytes())?;          let mac = match Macaroon::deserialize(&raw) {              Ok(m) => m, @@ -371,7 +377,12 @@ impl AuthConfectionary {          }))      } -    pub fn require_auth(&self, conn: &DbConn, auth_data: &Option<AuthData>, endpoint: Option<&str>) -> Result<AuthContext> { +    pub fn require_auth( +        &self, +        conn: &DbConn, +        auth_data: &Option<AuthData>, +        endpoint: Option<&str>, +    ) -> Result<AuthContext> {          match self.parse_swagger(conn, auth_data, endpoint)? {              Some(auth) => Ok(auth),              None => Err(ErrorKind::InvalidCredentials("no token supplied".to_string()).into()), | 
