diff --git a/conch.conf.dist b/conch.conf.dist index 4bee6e0bb..28e5b66ea 100644 --- a/conch.conf.dist +++ b/conch.conf.dist @@ -5,10 +5,12 @@ # old ones. secrets => ["hunter2"], - jwt => { - # time in seconds for a JWT to be valid before requiring refresh or re-auth + authentication => { + # time in seconds for a login token and/or persistent session cookie to be valid before requiring refresh or re-auth system_admin_expiry => 2592000, # 30 days normal_expiry => 86400, # 1 day + + # used for api tokens only custom_token_expiry => 86400*365*5, # 5 years }, diff --git a/cpanfile b/cpanfile index ebdb69427..febd7ab10 100644 --- a/cpanfile +++ b/cpanfile @@ -33,7 +33,7 @@ requires 'Net::DNS'; # not used directly, but Email::Valid sometimes demands requires 'experimental', '0.020'; # mojolicious and networking -requires 'Mojolicious', '8.15'; +requires 'Mojolicious', '8.31'; requires 'Mojo::Pg'; requires 'Mojo::JWT'; requires 'Mojolicious::Plugin::Util::RandomString', '0.07'; # memory leak: https://rt.cpan.org/Ticket/Display.html?id=125981 diff --git a/cpanfile.snapshot b/cpanfile.snapshot index d98d1cc1a..62ef0b4f9 100644 --- a/cpanfile.snapshot +++ b/cpanfile.snapshot @@ -2679,8 +2679,8 @@ DISTRIBUTIONS Mojolicious 8.03 SQL::Abstract 1.86 perl 5.010001 - Mojolicious-8.26 - pathname: S/SR/SRI/Mojolicious-8.26.tar.gz + Mojolicious-8.31 + pathname: S/SR/SRI/Mojolicious-8.31.tar.gz provides: Mojo undef Mojo::Asset undef @@ -2749,7 +2749,7 @@ DISTRIBUTIONS Mojo::UserAgent::Transactor undef Mojo::Util undef Mojo::WebSocket undef - Mojolicious 8.26 + Mojolicious 8.31 Mojolicious::Command undef Mojolicious::Command::Author::cpanify undef Mojolicious::Command::Author::generate undef diff --git a/docs/index.md b/docs/index.md index 16ac55950..96961e2af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,13 +35,13 @@ The majority of our endpoints consume and respond with JSON documents that conform to a set of JSON schema. These schema can be found in the [json-schema](json-schema) directory in the main repository, as well as on this documentation site. -Successful (http 2xx code) response structures are as described for each endpoint. +Successful (HTTP 2xx code) response structures are as described for each endpoint. Error responses will use: -- failure to validate query parameters: http 400, [response.json#/definitions/QueryParamsValidationError](json-schema/response.json#/definitions/QueryParamsValidationError) -- failure to validate request body payload: http 400, [response.json#/definitions/RequestValidationError](json-schema/response.json#/definitions/RequestValidationError) -- all other errors, unless specified: http 4xx, [response.json#/definitions/Error](json-schema/response.json#/definitions/Error) +- failure to validate query parameters: HTTP 400, [response.json#/definitions/QueryParamsValidationError](json-schema/response.json#/definitions/QueryParamsValidationError) +- failure to validate request body payload: HTTP 400, [response.json#/definitions/RequestValidationError](json-schema/response.json#/definitions/RequestValidationError) +- all other errors, unless specified: HTTP 4xx, [response.json#/definitions/Error](json-schema/response.json#/definitions/Error) Available routes are: diff --git a/docs/json-schema/request.json b/docs/json-schema/request.json index 1fae95d80..09cfec2a8 100644 --- a/docs/json-schema/request.json +++ b/docs/json-schema/request.json @@ -657,6 +657,10 @@ "password" : { "$ref" : "common.json#/definitions/non_empty_string" }, + "set_session" : { + "default" : false, + "type" : "boolean" + }, "user_id" : { "$ref" : "common.json#/definitions/uuid" } @@ -1085,6 +1089,30 @@ }, "type" : "object" }, + "UserIdOrEmail" : { + "additionalProperties" : true, + "oneOf" : [ + { + "required" : [ + "user_id" + ] + }, + { + "required" : [ + "email" + ] + } + ], + "properties" : { + "email" : { + "$ref" : "common.json#/definitions/email_address" + }, + "user_id" : { + "$ref" : "common.json#/definitions/uuid" + } + }, + "type" : "object" + }, "UserPassword" : { "additionalProperties" : false, "properties" : { diff --git a/docs/modules/Conch::Controller::Login.md b/docs/modules/Conch::Controller::Login.md index b445f9c4a..c7ffc3f4f 100644 --- a/docs/modules/Conch::Controller::Login.md +++ b/docs/modules/Conch::Controller::Login.md @@ -14,9 +14,8 @@ Create a response containing a login JWT, which the user should later present in Handle the details of authenticating the user, with one of the following options: ``` -* existing session for the user * signed JWT in the Authorization Bearer header -* Old 'conch' session cookie +* existing session for the user (using the 'conch' session cookie) ``` Does not terminate the connection if authentication is successful, allowing for chaining to @@ -29,11 +28,11 @@ Response uses the Login json schema, containing a JWT. ## logout -Logs a user out by expiring their session +Logs a user out by expiring their JWT and user session ## refresh\_token -Refresh a user's JWT token. Deletes the old token and expires the session. +Refresh a user's JWT token and persistent user session, deleting the old token. # LICENSING diff --git a/docs/modules/Conch::DB::Result::ValidationResult.md b/docs/modules/Conch::DB::Result::ValidationResult.md index 3879879e0..59b8008c8 100644 --- a/docs/modules/Conch::DB::Result::ValidationResult.md +++ b/docs/modules/Conch::DB::Result::ValidationResult.md @@ -93,6 +93,19 @@ size: 16 - ["id"](#id) +# UNIQUE CONSTRAINTS + +## `validation_result_all_columns_key` + +- ["device\_id"](#device_id) +- ["hardware\_product\_id"](#hardware_product_id) +- ["validation\_id"](#validation_id) +- ["message"](#message) +- ["hint"](#hint) +- ["status"](#status) +- ["category"](#category) +- ["component"](#component) + # RELATIONS ## device diff --git a/docs/modules/Conch::Plugin::DeprecatedAction.md b/docs/modules/Conch::Plugin::DeprecatedAction.md index f925c138e..92cdea581 100644 --- a/docs/modules/Conch::Plugin::DeprecatedAction.md +++ b/docs/modules/Conch::Plugin::DeprecatedAction.md @@ -12,7 +12,7 @@ Mojo plugin to detect and report the usage of deprecated controller actions. Sets the `X-Deprecated` header in the response. -Also sends a message to rollbar when a deprecated action is invoked, if the +Also sends a message to Rollbar when a deprecated action is invoked, if the `report_deprecated_actions` feature is enabled. # LICENSING diff --git a/docs/modules/Conch::Route.md b/docs/modules/Conch::Route.md index e8469832b..ea54a5d64 100644 --- a/docs/modules/Conch::Route.md +++ b/docs/modules/Conch::Route.md @@ -12,93 +12,108 @@ Set up all the routes for the Conch Mojo application. Set up the full route structure +# SHORTCUTS + +These are available on the root router. See ["Shortcuts" in Mojolicious::Guides::Routing](https://metacpan.org/pod/Mojolicious%3A%3AGuides%3A%3ARouting#shortcuts). + +## require\_system\_admin + +Chainable route that aborts with HTTP 403 if the user is not a system admin. + +## find\_user\_from\_payload + +Chainable route that looks up the user by `user_id` or `email` in the JSON payload, +aborting with HTTP 410 or HTTP 404 if not found. + +# ROUTE ENDPOINTS + Unless otherwise specified, all routes require authentication. Full access is granted to system admin users, regardless of workspace, build or other role entries. -Successful (http 2xx code) response structures are as described for each endpoint. +Successful (HTTP 2xx code) response structures are as described for each endpoint. Error responses will use: -- failure to validate query parameters: http 400, [response.json#/definitions/QueryParamsValidationError](../json-schema/response.json#/definitions/QueryParamsValidationError) -- failure to validate request body payload: http 400, [response.json#/RequestValidationError](../json-schema/response.json#/RequestValidationError) -- all other errors, unless specified: http 4xx, [response.json#/Error](../json-schema/response.json#/Error) +- failure to validate query parameters: HTTP 400, [response.json#/definitions/QueryParamsValidationError](../json-schema/response.json#/definitions/QueryParamsValidationError) +- failure to validate request body payload: HTTP 400, [response.json#/RequestValidationError](../json-schema/response.json#/RequestValidationError) +- all other errors, unless specified: HTTP 4xx, [response.json#/Error](../json-schema/response.json#/Error) -### `GET /ping` +## `GET /ping` - Does not require authentication. - Response: [response.json#/definitions/Ping](../json-schema/response.json#/definitions/Ping) -### `GET /version` +## `GET /version` - Does not require authentication. - Response: [response.json#/definitions/Version](../json-schema/response.json#/definitions/Version) -### `POST /login` +## `POST /login` - Request: [request.json#/definitions/Login](../json-schema/request.json#/definitions/Login) - Response: [response.json#/definitions/Login](../json-schema/response.json#/definitions/Login) -### `POST /logout` +## `POST /logout` - Does not require authentication. - Response: `204 NO CONTENT` -### `GET /workspace/:workspace/device-totals` +## `GET /workspace/:workspace/device-totals` -### `GET /workspace/:workspace/device-totals.circ` +## `GET /workspace/:workspace/device-totals.circ` - Does not require authentication. - Response: [response.json#/definitions/DeviceTotals](../json-schema/response.json#/definitions/DeviceTotals) - Response (Circonus): [response.json#/definitions/DeviceTotalsCirconus](../json-schema/response.json#/definitions/DeviceTotalsCirconus) -### `POST /refresh_token` +## `POST /refresh_token` - Request: [request.json#/definitions/Null](../json-schema/request.json#/definitions/Null) - Response: [response.json#/definitions/Login](../json-schema/response.json#/definitions/Login) -### `* /dc`, `* /room`, `* /rack_role`, `* /rack`, `* /layout` +## `* /dc`, `* /room`, `* /rack_role`, `* /rack`, `* /layout` See ["routes" in Conch::Route::Datacenter](../modules/Conch%3A%3ARoute%3A%3ADatacenter#routes) -### `* /device` +## `* /device` See ["routes" in Conch::Route::Device](../modules/Conch%3A%3ARoute%3A%3ADevice#routes) -### `* /device_report` +## `* /device_report` See ["routes" in Conch::Route::DeviceReport](../modules/Conch%3A%3ARoute%3A%3ADeviceReport#routes) -### `* /hardware_product` +## `* /hardware_product` See ["routes" in Conch::Route::HardwareProduct](../modules/Conch%3A%3ARoute%3A%3AHardwareProduct#routes) -### `* /hardware_vendor` +## `* /hardware_vendor` See ["routes" in Conch::Route::HardwareVendor](../modules/Conch%3A%3ARoute%3A%3AHardwareVendor#routes) -### `* /organization` +## `* /organization` See ["routes" in Conch::Route::Organization](../modules/Conch%3A%3ARoute%3A%3AOrganization#routes) -### `* /relay` +## `* /relay` See ["routes" in Conch::Route::Relay](../modules/Conch%3A%3ARoute%3A%3ARelay#routes) -### `* /schema` +## `* /schema` See ["routes" in Conch::Route::Schema](../modules/Conch%3A%3ARoute%3A%3ASchema#routes) -### `* /user` +## `* /user` See ["routes" in Conch::Route::User](../modules/Conch%3A%3ARoute%3A%3AUser#routes) -### `* /validation`, `* /validation_plan`, `* /validation_state` +## `* /validation`, `* /validation_plan`, `* /validation_state` See ["routes" in Conch::Route::Validation](../modules/Conch%3A%3ARoute%3A%3AValidation#routes) -### `* /workspace` +## `* /workspace` See ["routes" in Conch::Route::Workspace](../modules/Conch%3A%3ARoute%3A%3AWorkspace#routes) diff --git a/docs/modules/Conch::Route::Build.md b/docs/modules/Conch::Route::Build.md index fa1559fed..fd78e7916 100644 --- a/docs/modules/Conch::Route::Build.md +++ b/docs/modules/Conch::Route::Build.md @@ -8,9 +8,11 @@ Conch::Route::Build Sets up the routes for /build. +# ROUTE ENDPOINTS + All routes require authentication. -### `GET /build` +## `GET /build` Supports the following optional query parameters: @@ -20,13 +22,13 @@ Supports the following optional query parameters: - Response: response.yaml#/Builds -### `POST /build` +## `POST /build` - Requires system admin authorization - Request: request.yaml#/BuildCreate - Response: Redirect to the build -### `GET /build/:build_id_or_name` +## `GET /build/:build_id_or_name` Supports the following optional query parameters: @@ -37,18 +39,18 @@ Supports the following optional query parameters: - Requires system admin authorization or the read-only role on the build - Response: response.yaml#/Build -### `POST /build/:build_id_or_name` +## `POST /build/:build_id_or_name` - Requires system admin authorization or the admin role on the build - Request: request.yaml#/BuildUpdate - Response: Redirect to the build -### `GET /build/:build_id_or_name/user` +## `GET /build/:build_id_or_name/user` - Requires system admin authorization or the admin role on the build - Response: response.yaml#/BuildUsers -### `POST /build/:build_id_or_name/user?send_mail=<1|0`> +## `POST /build/:build_id_or_name/user?send_mail=<1|0`> Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the user. @@ -57,7 +59,7 @@ an email to the user. - Request: request.yaml#/BuildAddUser - Response: `204 NO CONTENT` -### `DELETE /build/:build_id_or_name/user/#target_user_id_or_email?send_mail=<1|0`> +## `DELETE /build/:build_id_or_name/user/#target_user_id_or_email?send_mail=<1|0`> Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the user. @@ -65,12 +67,12 @@ an email to the user. - Requires system admin authorization or the admin role on the build - Response: `204 NO CONTENT` -### `GET /build/:build_id_or_name/organization` +## `GET /build/:build_id_or_name/organization` - User requires the admin role - Response: [response.json#/definitions/BuildOrganizations](../json-schema/response.json#/definitions/BuildOrganizations) -### `POST /build/:build_id_or_name/organization?send_mail=<1|0>` +## `POST /build/:build_id_or_name/organization?send_mail=<1|0>` Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the organization members and build admins. @@ -79,7 +81,7 @@ an email to the organization members and build admins. - Request: [request.json#/definitions/BuildAddOrganization](../json-schema/request.json#/definitions/BuildAddOrganization) - Response: `204 NO CONTENT` -### `DELETE /build/:build_id_or_name/organization/:organization_id_or_name?send_mail=<1|0>` +## `DELETE /build/:build_id_or_name/organization/:organization_id_or_name?send_mail=<1|0>` Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the organization members and build admins. @@ -87,7 +89,7 @@ an email to the organization members and build admins. - User requires the admin role - Response: `204 NO CONTENT` -### `GET /build/:build_id_or_name/device` +## `GET /build/:build_id_or_name/device` Accepts the following optional query parameters: @@ -99,12 +101,12 @@ Accepts the following optional query parameters: - Requires system admin authorization or the read-only role on the build - Response: [response.json#/definitions/Devices](../json-schema/response.json#/definitions/Devices), [response.json#/definitions/DeviceIds](../json-schema/response.json#/definitions/DeviceIds) or [response.json#/definitions/DeviceSerials](../json-schema/response.json#/definitions/DeviceSerials) -### `GET /build/:build_id_or_name/device/pxe` +## `GET /build/:build_id_or_name/device/pxe` - Requires system admin authorization or the read-only role on the build - Response: [response.json#/definitions/DevicePXEs](../json-schema/response.json#/definitions/DevicePXEs) -### `POST /build/:build_id_or_name/device` +## `POST /build/:build_id_or_name/device` - Requires system admin authorization, or the read/write role on the build and the read-write role on existing device(s) (via a workspace or build; see @@ -112,23 +114,23 @@ read-write role on existing device(s) (via a workspace or build; see - Request: [request.json#/definitions/BuildCreateDevices](../json-schema/request.json#/definitions/BuildCreateDevices) - Response: `204 NO CONTENT` -### `POST /build/:build_id_or_name/device/:device_id_or_serial_number` +## `POST /build/:build_id_or_name/device/:device_id_or_serial_number` - Requires system admin authorization, or the read/write role on the build and the read-write role on the device (via a workspace or build; see ["routes" in Conch::Route::Device](../modules/Conch%3A%3ARoute%3A%3ADevice#routes)) - Response: `204 NO CONTENT` -### `DELETE /build/:build_id_or_name/device/:device_id_or_serial_number` +## `DELETE /build/:build_id_or_name/device/:device_id_or_serial_number` - Requires system admin authorization, or the read/write role on the build - Response: `204 NO CONTENT` -### `GET /build/:build_id_or_name/rack` +## `GET /build/:build_id_or_name/rack` - Requires system admin authorization or the read-only role on the build - Response: response.yaml#/Racks -### `POST /build/:build_id_or_name/rack/:rack_id_or_name` +## `POST /build/:build_id_or_name/rack/:rack_id_or_name` - Requires system admin authorization, or the read/write role on the build and the read-write role on a workspace or build that contains the rack diff --git a/docs/modules/Conch::Route::Datacenter.md b/docs/modules/Conch::Route::Datacenter.md index eaf793289..0533cf6b6 100644 --- a/docs/modules/Conch::Route::Datacenter.md +++ b/docs/modules/Conch::Route::Datacenter.md @@ -6,43 +6,45 @@ Conch::Route::Datacenter ## routes -Sets up the routes for /dc: +Sets up the routes for /dc. + +# ROUTE ENDPOINTS All routes require authentication. -### `GET /dc` +## `GET /dc` - Requires system admin authorization - Response: [response.json#/definitions/Datacenters](../json-schema/response.json#/definitions/Datacenters) -### `POST /dc` +## `POST /dc` - Requires system admin authorization - Request: [request.json#/definitions/DatacenterCreate](../json-schema/request.json#/definitions/DatacenterCreate) - Response: `201 CREATED` or `204 NO CONTENT`, plus Location header -### `GET /dc/:datacenter_id` +## `GET /dc/:datacenter_id` - Requires system admin authorization - Response: [response.json#/definitions/Datacenter](../json-schema/response.json#/definitions/Datacenter) -### `POST /dc/:datacenter_id` +## `POST /dc/:datacenter_id` - Requires system admin authorization - Request: [request.json#/definitions/DatacenterUpdate](../json-schema/request.json#/definitions/DatacenterUpdate) - Response: Redirect to the updated datacenter -### `DELETE /dc/:datacenter_id` +## `DELETE /dc/:datacenter_id` - Requires system admin authorization - Response: `204 NO CONTENT` -### `GET /dc/:datacenter_id/rooms` +## `GET /dc/:datacenter_id/rooms` - Requires system admin authorization - Response: [response.json#/definitions/DatacenterRoomsDetailed](../json-schema/response.json#/definitions/DatacenterRoomsDetailed) -### `GET /room` +## `GET /room` - Requires system admin authorization - Response: [response.json#/definitions/DatacenterRoomsDetailed](../json-schema/response.json#/definitions/DatacenterRoomsDetailed) diff --git a/docs/modules/Conch::Route::DatacenterRoom.md b/docs/modules/Conch::Route::DatacenterRoom.md index 7ced8697a..dcaf6c44a 100644 --- a/docs/modules/Conch::Route::DatacenterRoom.md +++ b/docs/modules/Conch::Route::DatacenterRoom.md @@ -6,83 +6,85 @@ Conch::Route::DatacenterRoom ## routes -Sets up the routes for /room: +Sets up the routes for /room. All routes require authentication. -### `GET /room` +# ROUTE ENDPOINTS + +## `GET /room` - Requires system admin authorization - Response: [response.json#/definitions/DatacenterRoomsDetailed](../json-schema/response.json#/definitions/DatacenterRoomsDetailed) -### `POST /room` +## `POST /room` - Requires system admin authorization - Request: [request.json#/definitions/DatacenterRoomCreate](../json-schema/request.json#/definitions/DatacenterRoomCreate) - Response: Redirect to the created room -### `GET /room/:datacenter_room_id_or_alias` +## `GET /room/:datacenter_room_id_or_alias` - User requires system admin authorization, or the read-only role on a rack located in the room - Response: [response.json#/definitions/DatacenterRoomDetailed](../json-schema/response.json#/definitions/DatacenterRoomDetailed) -### `POST /room/:datacenter_room_id_or_alias` +## `POST /room/:datacenter_room_id_or_alias` - Requires system admin authorization - Request: [request.json#/definitions/DatacenterRoomUpdate](../json-schema/request.json#/definitions/DatacenterRoomUpdate) - Response: Redirect to the updated room -### `DELETE /room/:datacenter_room_id_or_alias` +## `DELETE /room/:datacenter_room_id_or_alias` - Requires system admin authorization - Response: `204 NO CONTENT` -### `GET /room/:datacenter_room_id_or_alias/rack` +## `GET /room/:datacenter_room_id_or_alias/rack` - User requires system admin authorization, or the read-only role on a rack located in the room (in which case data returned is restricted to those racks) - Response: [response.json#/definitions/Racks](../json-schema/response.json#/definitions/Racks) -### `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name` +## `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name` - User requires the read-only role on the rack - Response: [response.json#/definitions/Rack](../json-schema/response.json#/definitions/Rack) -### `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name` +## `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name` - User requires the read/write role on the rack - Request: [request.json#/definitions/RackUpdate](../json-schema/request.json#/definitions/RackUpdate) - Response: Redirect to the updated rack -### `DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name` +## `DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name` - Requires system admin authorization - Response: `204 NO CONTENT` -### `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layouts` +## `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layouts` - User requires the read-only role on the rack - Response: [response.json#/definitions/RackLayouts](../json-schema/response.json#/definitions/RackLayouts) -### `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layouts` +## `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layouts` - User requires the read/write role on the rack - Request: [request.json#/definitions/RackLayouts](../json-schema/request.json#/definitions/RackLayouts) - Response: Redirect to the rack's layouts -### `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment` +## `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment` - User requires the read-only role on the rack - Response: [response.json#/definitions/RackAssignments](../json-schema/response.json#/definitions/RackAssignments) -### `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment` +## `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment` - User requires the read/write role on the rack - Request: [request.json#/definitions/RackAssignmentUpdates](../json-schema/request.json#/definitions/RackAssignmentUpdates) - Response: Redirect to the updated rack assignment -### `DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment` +## `DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment` This method requires a request body. @@ -90,7 +92,7 @@ This method requires a request body. - Request: [request.json#/definitions/RackAssignmentDeletes](../json-schema/request.json#/definitions/RackAssignmentDeletes) - Response: `204 NO CONTENT` -### `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/phase?rack_only=<0|1>` +## `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/phase?rack_only=<0|1>` The query parameter `rack_only` (defaults to `0`) specifies whether to update only the rack's phase, or all the rack's devices' phases as well. @@ -99,15 +101,15 @@ only the rack's phase, or all the rack's devices' phases as well. - Request: [request.json#/definitions/RackPhase](../json-schema/request.json#/definitions/RackPhase) - Response: Redirect to the updated rack -### `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` +## `GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` See ["`GET /layout/:layout_id`" in Conch::Route::RackLayout](../modules/Conch%3A%3ARoute%3A%3ARackLayout#get-layoutlayout_id). -### `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` +## `POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` See ["`POST /layout/:layout_id`" in Conch::Route::RackLayout](../modules/Conch%3A%3ARoute%3A%3ARackLayout#post-layoutlayout_id). -### `DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` +## `DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` See ["`DELETE /layout/:layout_id`" in Conch::Route::RackLayout](../modules/Conch%3A%3ARoute%3A%3ARackLayout#delete-layoutlayout_id). diff --git a/docs/modules/Conch::Route::Device.md b/docs/modules/Conch::Route::Device.md index ea679e131..45746bd6e 100644 --- a/docs/modules/Conch::Route::Device.md +++ b/docs/modules/Conch::Route::Device.md @@ -6,7 +6,9 @@ Conch::Route::Device ## routes -Sets up the routes for /device: +Sets up the routes for /device. + +# ROUTE ENDPOINTS All routes require authentication. @@ -19,12 +21,12 @@ a [role](../modules/Conch%3A%3ADB%3A%3AResult%3A%3AUserWorkspaceRole#role) in th Full (admin-level) access is also granted to a device if a report was sent for that device using a relay that registered with that user's credentials. -### `POST /device/:device_serial_number` +## `POST /device/:device_serial_number` - Request: [device_report.json#/definitions/DeviceReport](../json-schema/device_report.json#/definitions/DeviceReport) - Response: [response.json#/definitions/ValidationStateWithResults](../json-schema/response.json#/definitions/ValidationStateWithResults) -### `GET /device?:key=:value` +## `GET /device?:key=:value` Supports the following query parameters: @@ -39,108 +41,108 @@ below. - Response: [response.json#/definitions/Devices](../json-schema/response.json#/definitions/Devices) -### `GET /device/:device_id_or_serial_number` +## `GET /device/:device_id_or_serial_number` - User requires the read-only role - Response: [response.json#/definitions/DetailedDevice](../json-schema/response.json#/definitions/DetailedDevice) -### `GET /device/:device_id_or_serial_number/pxe` +## `GET /device/:device_id_or_serial_number/pxe` - User requires the read-only role - Response: [response.json#/definitions/DevicePXE](../json-schema/response.json#/definitions/DevicePXE) -### `GET /device/:device_id_or_serial_number/phase` +## `GET /device/:device_id_or_serial_number/phase` - User requires the read-only role - Response: [response.json#/definitions/DevicePhase](../json-schema/response.json#/definitions/DevicePhase) -### `GET /device/:device_id_or_serial_number/sku` +## `GET /device/:device_id_or_serial_number/sku` - User requires the read-only role - Response: [response.json#/definitions/DeviceSku](../json-schema/response.json#/definitions/DeviceSku) -### `POST /device/:device_id_or_serial_number/asset_tag` +## `POST /device/:device_id_or_serial_number/asset_tag` - User requires the read/write role - Request: [request.json#/definitions/DeviceAssetTag](../json-schema/request.json#/definitions/DeviceAssetTag) - Response: Redirect to the updated device -### `POST /device/:device_id_or_serial_number/validated` +## `POST /device/:device_id_or_serial_number/validated` - User requires the read/write role - Request: [request.json#/definitions/Null](../json-schema/request.json#/definitions/Null) - Response: Redirect to the updated device -### `POST /device/:device_id_or_serial_number/phase` +## `POST /device/:device_id_or_serial_number/phase` - User requires the read/write role - Request: [request.json#/definitions/DevicePhase](../json-schema/request.json#/definitions/DevicePhase) - Response: Redirect to the updated device -### `POST /device/:device_id_or_serial_number/links` +## `POST /device/:device_id_or_serial_number/links` - User requires the read/write role - Request: [request.json#/definitions/DeviceLinks](../json-schema/request.json#/definitions/DeviceLinks) - Response: Redirect to the updated device -### `DELETE /device/:device_id_or_serial_number/links` +## `DELETE /device/:device_id_or_serial_number/links` - User requires the read/write role - Response: 204 NO CONTENT -### `POST /device/:device_id_or_serial_number/build` +## `POST /device/:device_id_or_serial_number/build` - User requires the read/write role for the device, as well as the old and new builds - Request: [request.json#/definitions/DeviceBuild](../json-schema/request.json#/definitions/DeviceBuild) - Response: Redirect to the updated device -### `GET /device/:device_id_or_serial_number/location` +## `GET /device/:device_id_or_serial_number/location` - User requires the read-only role - Response: [response.json#/definitions/DeviceLocation](../json-schema/response.json#/definitions/DeviceLocation) -### `POST /device/:device_id_or_serial_number/location` +## `POST /device/:device_id_or_serial_number/location` - User requires the read/write role - Request: [request.json#/definitions/DeviceLocationUpdate](../json-schema/request.json#/definitions/DeviceLocationUpdate) - Response: Redirect to the updated device -### `DELETE /device/:device_id_or_serial_number/location` +## `DELETE /device/:device_id_or_serial_number/location` - User requires the read/write role - Response: `204 NO CONTENT` -### `GET /device/:device_id_or_serial_number/settings` +## `GET /device/:device_id_or_serial_number/settings` - User requires the read-only role - Response: [response.json#/definitions/DeviceSettings](../json-schema/response.json#/definitions/DeviceSettings) -### `POST /device/:device_id_or_serial_number/settings` +## `POST /device/:device_id_or_serial_number/settings` - User requires the read/write role, or admin when overwriting existing settings that do not start with `tag.`. - Request: [request.json#/definitions/DeviceSettings](../json-schema/request.json#/definitions/DeviceSettings) - Response: `204 NO CONTENT` -### `GET /device/:device_id_or_serial_number/settings/:key` +## `GET /device/:device_id_or_serial_number/settings/:key` - User requires the read-only role - Response: [response.json#/definitions/DeviceSetting](../json-schema/response.json#/definitions/DeviceSetting) -### `POST /device/:device_id_or_serial_number/settings/:key` +## `POST /device/:device_id_or_serial_number/settings/:key` - User requires the read/write role, or admin when overwriting existing settings that do not start with `tag.`. - Request: [request.json#/definitions/DeviceSettings](../json-schema/request.json#/definitions/DeviceSettings) - Response: `204 NO CONTENT` -### `DELETE /device/:device_id_or_serial_number/settings/:key` +## `DELETE /device/:device_id_or_serial_number/settings/:key` - User requires the read/write role for settings that start with `tag.`, and admin otherwise. - Response: `204 NO CONTENT` -### `POST /device/:device_id_or_serial_number/validation/:validation_id` +## `POST /device/:device_id_or_serial_number/validation/:validation_id` Does not store validation results. @@ -148,7 +150,7 @@ Does not store validation results. - Request: [device_report.json#/definitions/DeviceReport](../json-schema/device_report.json#/definitions/DeviceReport) - Response: [response.json#/definitions/ValidationResults](../json-schema/response.json#/definitions/ValidationResults) -### `POST /device/:device_id_or_serial_number/validation_plan/:validation_plan_id` +## `POST /device/:device_id_or_serial_number/validation_plan/:validation_plan_id` Does not store validation results. @@ -156,7 +158,7 @@ Does not store validation results. - Request: [device_report.json#/definitions/DeviceReport](../json-schema/device_report.json#/definitions/DeviceReport) - Response: [response.json#/definitions/ValidationResults](../json-schema/response.json#/definitions/ValidationResults) -### `GET /device/:device_id_or_serial_number/validation_state?status=&status=...` +## `GET /device/:device_id_or_serial_number/validation_state?status=&status=...` Accepts the query parameter `status`, indicating the desired status(es) to search for (one of `pass`, `fail`, `error`). Can be used more than once. @@ -164,17 +166,17 @@ to search for (one of `pass`, `fail`, `error`). Can be used more than once. - User requires the read-only role - Response: [response.json#/definitions/ValidationStatesWithResults](../json-schema/response.json#/definitions/ValidationStatesWithResults) -### `GET /device/:device_id_or_serial_number/interface` +## `GET /device/:device_id_or_serial_number/interface` - User requires the read-only role - Response: [response.json#/definitions/DeviceNics](../json-schema/response.json#/definitions/DeviceNics) -### `GET /device/:device_id_or_serial_number/interface/:interface_name` +## `GET /device/:device_id_or_serial_number/interface/:interface_name` - User requires the read-only role - Response: [response.json#/definitions/DeviceNic](../json-schema/response.json#/definitions/DeviceNic) -### `GET /device/:device_id_or_serial_number/interface/:interface_name/:field` +## `GET /device/:device_id_or_serial_number/interface/:interface_name/:field` - User requires the read-only role - Response: [response.json#/definitions/DeviceNicField](../json-schema/response.json#/definitions/DeviceNicField) diff --git a/docs/modules/Conch::Route::DeviceReport.md b/docs/modules/Conch::Route::DeviceReport.md index 46d90d683..473dea9dd 100644 --- a/docs/modules/Conch::Route::DeviceReport.md +++ b/docs/modules/Conch::Route::DeviceReport.md @@ -6,16 +6,18 @@ Conch::Route::DeviceReport ## routes -Sets up the routes for /device\_report: +Sets up the routes for /device\_report. + +# ROUTE ENDPOINTS All routes require authentication. -### `POST /device_report` +## `POST /device_report` - Request: [device_report.json#/definitions/DeviceReport](../json-schema/device_report.json#/definitions/DeviceReport) - Response: [response.json#/definitions/ReportValidationResults](../json-schema/response.json#/definitions/ReportValidationResults) -### `GET /device_report/:device_report_id` +## `GET /device_report/:device_report_id` - User requires the read-only role, as described in ["routes" in Conch::Route::Device](../modules/Conch%3A%3ARoute%3A%3ADevice#routes). - Response: [response.json#/definitions/DeviceReportRow](../json-schema/response.json#/definitions/DeviceReportRow) diff --git a/docs/modules/Conch::Route::HardwareProduct.md b/docs/modules/Conch::Route::HardwareProduct.md index f20b2781b..2496f5337 100644 --- a/docs/modules/Conch::Route::HardwareProduct.md +++ b/docs/modules/Conch::Route::HardwareProduct.md @@ -6,27 +6,29 @@ Conch::Route::HardwareProduct ## routes -Sets up the routes for /hardware\_product: +Sets up the routes for /hardware\_product. + +# ROUTE ENDPOINTS All routes require authentication. -### `GET /hardware_product` +## `GET /hardware_product` - Response: [response.json#/definitions/HardwareProducts](../json-schema/response.json#/definitions/HardwareProducts) -### `POST /hardware_product` +## `POST /hardware_product` - Requires system admin authorization - Request: [request.json#/definitions/HardwareProductCreate](../json-schema/request.json#/definitions/HardwareProductCreate) - Response: Redirect to the created hardware product -### `GET /hardware_product/:hardware_product_id_or_other` +## `GET /hardware_product/:hardware_product_id_or_other` Identifiers accepted: `id`, `sku`, `name` and `alias`. - Response: [response.json#/definitions/HardwareProduct](../json-schema/response.json#/definitions/HardwareProduct) -### `POST /hardware_product/:hardware_product_id_or_other` +## `POST /hardware_product/:hardware_product_id_or_other` Identifiers accepted: `id`, `sku`, `name` and `alias`. @@ -34,7 +36,7 @@ Identifiers accepted: `id`, `sku`, `name` and `alias`. - Request: [request.json#/definitions/HardwareProductUpdate](../json-schema/request.json#/definitions/HardwareProductUpdate) - Response: Redirect to the updated hardware product -### `DELETE /hardware_product/:hardware_product_id_or_other` +## `DELETE /hardware_product/:hardware_product_id_or_other` Identifiers accepted: `id`, `sku`, `name` and `alias`. diff --git a/docs/modules/Conch::Route::HardwareVendor.md b/docs/modules/Conch::Route::HardwareVendor.md index baf156920..4d763bd99 100644 --- a/docs/modules/Conch::Route::HardwareVendor.md +++ b/docs/modules/Conch::Route::HardwareVendor.md @@ -6,24 +6,26 @@ Conch::Route::HardwareVendor ## routes -Sets up the routes for /hardware\_vendor: +Sets up the routes for /hardware\_vendor. + +# ROUTE ENDPOINTS All routes require authentication. -### `GET /hardware_vendor` +## `GET /hardware_vendor` - Response: [response.json#/definitions/HardwareVendors](../json-schema/response.json#/definitions/HardwareVendors) -### `GET /hardware_vendor/:hardware_vendor_id_or_name` +## `GET /hardware_vendor/:hardware_vendor_id_or_name` - Response: [response.json#/definitions/HardwareVendor](../json-schema/response.json#/definitions/HardwareVendor) -### `DELETE /hardware_vendor/:hardware_vendor_id_or_name` +## `DELETE /hardware_vendor/:hardware_vendor_id_or_name` - Requires system admin authorization - Response: `204 NO CONTENT` -### `POST /hardware_vendor/:hardware_vendor_name` +## `POST /hardware_vendor/:hardware_vendor_name` - Requires system admin authorization - Request: [request.json#/definitions/Null](../json-schema/request.json#/definitions/Null) diff --git a/docs/modules/Conch::Route::Organization.md b/docs/modules/Conch::Route::Organization.md index 6c0f2b5d2..50b0e85ad 100644 --- a/docs/modules/Conch::Route::Organization.md +++ b/docs/modules/Conch::Route::Organization.md @@ -8,35 +8,37 @@ Conch::Route::Organization Sets up the routes for /organization. +# ROUTE ENDPOINTS + All routes require authentication. -### `GET /organization` +## `GET /organization` - Response: [response.json#/definitions/Organizations](../json-schema/response.json#/definitions/Organizations) -### `POST /organization` +## `POST /organization` - Requires system admin authorization - Request: [request.json#/definitions/OrganizationCreate](../json-schema/request.json#/definitions/OrganizationCreate) - Response: Redirect to the organization -### `GET /organization/:organization_id_or_name` +## `GET /organization/:organization_id_or_name` - Requires system admin authorization or the admin role on the organization - Response: [response.json#/definitions/Organization](../json-schema/response.json#/definitions/Organization) -### `POST /organization/:organization_id_or_name` +## `POST /organization/:organization_id_or_name` - Requires system admin authorization or the admin role on the organization - Request: request.yaml#/OrganizationUpdate - Response: Redirect to the organization -### `DELETE /organization/:organization_id_or_name` +## `DELETE /organization/:organization_id_or_name` - Requires system admin authorization - Response: `204 NO CONTENT` -### `POST /organization/:organization_id_or_name/user?send_mail=<1|0`> +## `POST /organization/:organization_id_or_name/user?send_mail=<1|0`> Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the user. @@ -45,7 +47,7 @@ an email to the user. - Request: [request.json#/definitions/OrganizationAddUser](../json-schema/request.json#/definitions/OrganizationAddUser) - Response: `204 NO CONTENT` -### `DELETE /organization/:organization_id_or_name/user/#target_user_id_or_email?send_mail=<1|0`> +## `DELETE /organization/:organization_id_or_name/user/#target_user_id_or_email?send_mail=<1|0`> Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the user. diff --git a/docs/modules/Conch::Route::Rack.md b/docs/modules/Conch::Route::Rack.md index ce0b11e5b..46dc5d027 100644 --- a/docs/modules/Conch::Route::Rack.md +++ b/docs/modules/Conch::Route::Rack.md @@ -6,63 +6,65 @@ Conch::Route::Rack ## routes -Sets up the routes for /rack: +Sets up the routes for /rack. ## one\_rack\_routes Sets up the routes for working with just one rack, mounted under a provided route prefix. +# ROUTE ENDPOINTS + All routes require authentication. Take note: All routes that reference a specific rack (prefix `/rack/:rack_id`) are also available under `/rack/:rack_id_or_long_name` as well as `/room/datacenter_room_id_or_alias/rack/:rack_id_or_name`. -### `POST /rack` +## `POST /rack` - Requires system admin authorization - Request: [request.json#/definitions/RackCreate](../json-schema/request.json#/definitions/RackCreate) - Response: Redirect to the created rack -### `GET /rack/:rack_id_or_name` +## `GET /rack/:rack_id_or_name` - User requires the read-only role on the rack - Response: [response.json#/definitions/Rack](../json-schema/response.json#/definitions/Rack) -### `POST /rack/:rack_id_or_name` +## `POST /rack/:rack_id_or_name` - User requires the read/write role on the rack - Request: [request.json#/definitions/RackUpdate](../json-schema/request.json#/definitions/RackUpdate) - Response: Redirect to the updated rack -### `DELETE /rack/:rack_id_or_name` +## `DELETE /rack/:rack_id_or_name` - Requires system admin authorization - Response: `204 NO CONTENT` -### `GET /rack/:rack_id_or_name/layout` +## `GET /rack/:rack_id_or_name/layout` - User requires the read-only role on the rack - Response: [response.json#/definitions/RackLayouts](../json-schema/response.json#/definitions/RackLayouts) -### `POST /rack/:rack_id_or_name/layout` +## `POST /rack/:rack_id_or_name/layout` - User requires the read/write role on the rack - Request: [request.json#/definitions/RackLayouts](../json-schema/request.json#/definitions/RackLayouts) - Response: Redirect to the rack's layouts -### `GET /rack/:rack_id_or_name/assignment` +## `GET /rack/:rack_id_or_name/assignment` - User requires the read-only role on the rack - Response: [response.json#/definitions/RackAssignments](../json-schema/response.json#/definitions/RackAssignments) -### `POST /rack/:rack_id_or_name/assignment` +## `POST /rack/:rack_id_or_name/assignment` - User requires the read/write role on the rack - Request: [request.json#/definitions/RackAssignmentUpdates](../json-schema/request.json#/definitions/RackAssignmentUpdates) - Response: Redirect to the updated rack assignment -### `DELETE /rack/:rack_id_or_name/assignment` +## `DELETE /rack/:rack_id_or_name/assignment` This method requires a request body. @@ -70,7 +72,7 @@ This method requires a request body. - Request: [request.json#/definitions/RackAssignmentDeletes](../json-schema/request.json#/definitions/RackAssignmentDeletes) - Response: `204 NO CONTENT` -### `POST /rack/:rack_id_or_name/phase?rack_only=<0|1>` +## `POST /rack/:rack_id_or_name/phase?rack_only=<0|1>` The query parameter `rack_only` (defaults to `0`) specifies whether to update only the rack's phase, or all the rack's devices' phases as well. @@ -79,15 +81,15 @@ only the rack's phase, or all the rack's devices' phases as well. - Request: [request.json#/definitions/RackPhase](../json-schema/request.json#/definitions/RackPhase) - Response: Redirect to the updated rack -### `GET /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` +## `GET /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` See ["`GET /layout/:layout_id`" in Conch::Route::RackLayout](../modules/Conch%3A%3ARoute%3A%3ARackLayout#get-layoutlayout_id). -### `POST /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` +## `POST /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` See ["`POST /layout/:layout_id`" in Conch::Route::RackLayout](../modules/Conch%3A%3ARoute%3A%3ARackLayout#post-layoutlayout_id). -### `DELETE /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` +## `DELETE /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start` See ["`DELETE /layout/:layout_id`" in Conch::Route::RackLayout](../modules/Conch%3A%3ARoute%3A%3ARackLayout#delete-layoutlayout_id). diff --git a/docs/modules/Conch::Route::RackLayout.md b/docs/modules/Conch::Route::RackLayout.md index 9e8937454..437ad0c00 100644 --- a/docs/modules/Conch::Route::RackLayout.md +++ b/docs/modules/Conch::Route::RackLayout.md @@ -6,37 +6,44 @@ Conch::Route::RackLayout ## routes -Sets up the routes for /layout: +Sets up the routes for /layout. ## one\_layout\_routes Sets up the routes for working with just one layout, mounted under a provided route prefix. +# ROUTE ENDPOINTS + All routes require authentication. -### `GET /layout` +Take note: All routes that reference a specific rack layout (prefix `/layout/:layout_id`) are +also available under `/rack/:rack_id_or_long_name/layout/:layout_id_or_rack_unit_start` as +well as +`/room/datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start`. + +## `GET /layout` - Requires system admin authorization - Response: [response.json#/definitions/RackLayouts](../json-schema/response.json#/definitions/RackLayouts) -### `POST /layout` +## `POST /layout` - Requires system admin authorization - Request: [request.json#/definitions/RackLayoutCreate](../json-schema/request.json#/definitions/RackLayoutCreate) - Response: Redirect to the created rack layout -### `GET /layout/:layout_id` +## `GET /layout/:layout_id` - Requires system admin authorization - Response: [response.json#/definitions/RackLayout](../json-schema/response.json#/definitions/RackLayout) -### `POST /layout/:layout_id` +## `POST /layout/:layout_id` - Requires system admin authorization - Request: [request.json#/definitions/RackLayoutUpdate](../json-schema/request.json#/definitions/RackLayoutUpdate) - Response: Redirect to the update rack layout -### `DELETE /layout/:layout_id` +## `DELETE /layout/:layout_id` - Requires system admin authorization - Response: `204 NO CONTENT` diff --git a/docs/modules/Conch::Route::RackRole.md b/docs/modules/Conch::Route::RackRole.md index a21a852fe..0da2c50fc 100644 --- a/docs/modules/Conch::Route::RackRole.md +++ b/docs/modules/Conch::Route::RackRole.md @@ -6,31 +6,33 @@ Conch::Route::RackRole ## routes -Sets up the routes for /rack\_role: +Sets up the routes for /rack\_role. + +# ROUTE ENDPOINTS All routes require authentication. -### `GET /rack_role` +## `GET /rack_role` - Response: [response.json#/definitions/RackRoles](../json-schema/response.json#/definitions/RackRoles) -### `POST /rack_role` +## `POST /rack_role` - Requires system admin authorization - Request: [request.json#/definitions/RackRoleCreate](../json-schema/request.json#/definitions/RackRoleCreate) - Response: Redirect to the created rack role -### `GET /rack_role/:rack_role_id_or_name` +## `GET /rack_role/:rack_role_id_or_name` - Response: [response.json#/definitions/RackRole](../json-schema/response.json#/definitions/RackRole) -### `POST /rack_role/:rack_role_id_or_name` +## `POST /rack_role/:rack_role_id_or_name` - Requires system admin authorization - Request: [request.json#/definitions/RackRoleUpdate](../json-schema/request.json#/definitions/RackRoleUpdate) - Response: Redirect to the updated rack role -### `DELETE /rack_role/:rack_role_id_or_name` +## `DELETE /rack_role/:rack_role_id_or_name` - Requires system admin authorization - Response: `204 NO CONTENT` diff --git a/docs/modules/Conch::Route::Relay.md b/docs/modules/Conch::Route::Relay.md index 81178da61..cbfe70e0a 100644 --- a/docs/modules/Conch::Route::Relay.md +++ b/docs/modules/Conch::Route::Relay.md @@ -6,21 +6,23 @@ Conch::Route::Relay ## routes -Sets up the routes for /relay: +Sets up the routes for /relay. + +# ROUTE ENDPOINTS All routes require authentication. -### `POST /relay/:relay_serial_number/register` +## `POST /relay/:relay_serial_number/register` - Request: [request.json#/definitions/RegisterRelay](../json-schema/request.json#/definitions/RegisterRelay) - Response: `201 CREATED` or `204 NO CONTENT`, plus Location header -### `GET /relay` +## `GET /relay` - Requires system admin authorization - Response: [response.json#/definitions/Relays](../json-schema/response.json#/definitions/Relays) -### `GET /relay/:relay_id_or_serial_number` +## `GET /relay/:relay_id_or_serial_number` - Requires system admin authorization, or the user to have previously registered the relay. - Response: [response.json#/definitions/Relay](../json-schema/response.json#/definitions/Relay) diff --git a/docs/modules/Conch::Route::Schema.md b/docs/modules/Conch::Route::Schema.md index c61e0d804..97b950889 100644 --- a/docs/modules/Conch::Route::Schema.md +++ b/docs/modules/Conch::Route::Schema.md @@ -8,11 +8,13 @@ Conch::Route::Schema Sets up the routes for /schema. -### `GET /schema/query_params/:schema_name` +# ROUTE ENDPOINTS -### `GET /schema/request/:schema_name` +## `GET /schema/query_params/:schema_name` -### `GET /schema/response/:schema_name` +## `GET /schema/request/:schema_name` + +## `GET /schema/response/:schema_name` Returns the schema specified by type and name. diff --git a/docs/modules/Conch::Route::User.md b/docs/modules/Conch::Route::User.md index da52a8e54..3cecab2c7 100644 --- a/docs/modules/Conch::Route::User.md +++ b/docs/modules/Conch::Route::User.md @@ -6,15 +6,27 @@ Conch::Route::User ## routes -Sets up the routes for /user: +Sets up the routes for /user. + +# ROUTE ENDPOINTS All routes require authentication. -### `GET /user/me` +## `GET /user/me` - Response: [response.json#/definitions/UserDetailed](../json-schema/response.json#/definitions/UserDetailed) -### `POST /user/me/revoke?send_mail=<1|0>&login_only=<0|1>&api_only=<0|1>` +## `POST /user/:target_user_id_or_email?send_mail=<1|0>` + +Optionally take the query parameter `send_mail` (defaults to `1`) to send +an email telling the user their account was updated + +- Request: [request.json#/definitions/UpdateUser](../json-schema/request.json#/definitions/UpdateUser) +- Success Response: Redirect to the user that was updated +- Error response on duplicate user: [response.json#/definitions/UserError](../json-schema/response.json#/definitions/UserError) (only if the +calling user is a system admin) + +## `POST /user/me/revoke?send_mail=<1|0>&login_only=<0|1>&api_only=<0|1>` Optionally accepts the following query parameters: @@ -28,7 +40,7 @@ By default it will revoke both login/session and API tokens. - Request: [request.json#/definitions/UserSettings](../json-schema/request.json#/definitions/UserSettings) - Response: `204 NO CONTENT` -### `POST /user/me/password?clear_tokens=` +## `POST /user/me/password?clear_tokens=` Optionally takes a query parameter `clear_tokens`, to also revoke the session tokens for the user, forcing the user to log in again. Possible options are: @@ -43,61 +55,62 @@ otherwise, the user is logged out. - Request: [request.json#/definitions/UserSettings](../json-schema/request.json#/definitions/UserSettings) - Response: `204 NO CONTENT` -### `GET /user/me/settings` +## `GET /user/me/settings` - Response: [response.json#/definitions/UserSettings](../json-schema/response.json#/definitions/UserSettings) -### `POST /user/me/settings` +## `POST /user/me/settings` - Request: [request.json#/definitions/UserSettings](../json-schema/request.json#/definitions/UserSettings) - Response: `204 NO CONTENT` -### `GET /user/me/settings/:key` +## `GET /user/me/settings/:key` - Response: [response.json#/definitions/UserSetting](../json-schema/response.json#/definitions/UserSetting) -### `POST /user/me/settings/:key` +## `POST /user/me/settings/:key` - Request: [request.json#/definitions/UserSetting](../json-schema/request.json#/definitions/UserSetting) - Response: `204 NO CONTENT` -### `DELETE /user/me/settings/:key` +## `DELETE /user/me/settings/:key` - Response: `204 NO CONTENT` -### `GET /user/me/token` +## `GET /user/me/token` - Response: [response.json#/definitions/UserTokens](../json-schema/response.json#/definitions/UserTokens) -### `POST /user/me/token` +## `POST /user/me/token` - Request: [request.json#/definitions/NewUserToken](../json-schema/request.json#/definitions/NewUserToken) - Response: [response.json#/definitions/NewUserToken](../json-schema/response.json#/definitions/NewUserToken) -### `GET /user/me/token/:token_name` +## `GET /user/me/token/:token_name` - Response: [response.json#/definitions/UserToken](../json-schema/response.json#/definitions/UserToken) -### `DELETE /user/me/token/:token_name` +## `DELETE /user/me/token/:token_name` - Response: `204 NO CONTENT` -### `GET /user/:target_user_id_or_email` +## `GET /user/:target_user_id_or_email` -- Requires system admin authorization +- Requires system admin authorization (when updating a different account than one's own) - Response: [response.json#/definitions/UserDetailed](../json-schema/response.json#/definitions/UserDetailed) -### `POST /user/:target_user_id_or_email?send_mail=<1|0>` +## `POST /user/:target_user_id_or_email?send_mail=<1|0>` Optionally take the query parameter `send_mail` (defaults to `1`) to send -an email telling the user their tokens were revoked +an email telling the user their account was updated - Requires system admin authorization - Request: [request.json#/definitions/UpdateUser](../json-schema/request.json#/definitions/UpdateUser) - Success Response: Redirect to the user that was updated -- Error response on duplicate user: [response.json#/definitions/UserError](../json-schema/response.json#/definitions/UserError) +- Error response on duplicate user: [response.json#/definitions/UserError](../json-schema/response.json#/definitions/UserError) (only if the +calling user is a system admin) -### `DELETE /user/:target_user_id_or_email?clear_tokens=<1|0>` +## `DELETE /user/:target_user_id_or_email?clear_tokens=<1|0>` When a user is deleted, all role entries (workspace, build, organization) are removed and are unrecoverable. @@ -108,7 +121,7 @@ revoke all session tokens for the user forcing all tools to log in again. - Requires system admin authorization - Response: `204 NO CONTENT` -### `POST /user/:target_user_id_or_email/revoke?login_only=<0|1>&api_only=<0|1>` +## `POST /user/:target_user_id_or_email/revoke?login_only=<0|1>&api_only=<0|1>` Optionally accepts the following query parameters: @@ -121,7 +134,7 @@ By default it will revoke both login/session and API tokens. If both - Requires system admin authorization - Response: `204 NO CONTENT` -### `DELETE /user/:target_user_id_or_email/password?clear_tokens=&send_mail=<1|0>` +## `DELETE /user/:target_user_id_or_email/password?clear_tokens=&send_mail=<1|0>` Optionally accepts the following query parameters: @@ -134,12 +147,12 @@ Optionally accepts the following query parameters: - Requires system admin authorization - Response: `204 NO CONTENT` -### `GET /user` +## `GET /user` - Requires system admin authorization - Response: [response.json#/definitions/UsersDetailed](../json-schema/response.json#/definitions/UsersDetailed) -### `POST /user?send_mail=<1|0>` +## `POST /user?send_mail=<1|0>` Optionally takes a query parameter, `send_mail` (defaults to `1`) to send an email to the user with the new password. @@ -149,17 +162,17 @@ email to the user with the new password. - Success Response: [response.json#/definitions/User](../json-schema/response.json#/definitions/User) - Error response on duplicate user: [response.json#/definitions/UserError](../json-schema/response.json#/definitions/UserError) -### `GET /user/:target_user_id_or_email/token` +## `GET /user/:target_user_id_or_email/token` - Requires system admin authorization - Response: [response.json#/definitions/UserTokens](../json-schema/response.json#/definitions/UserTokens) -### `GET /user/:target_user_id_or_email/token/:token_name` +## `GET /user/:target_user_id_or_email/token/:token_name` - Requires system admin authorization - Response: [response.json#/definitions/UserTokens](../json-schema/response.json#/definitions/UserTokens) -### `DELETE /user/:target_user_id_or_email/token/:token_name` +## `DELETE /user/:target_user_id_or_email/token/:token_name` - Requires system admin authorization - Success Response: `204 NO CONTENT` diff --git a/docs/modules/Conch::Route::Validation.md b/docs/modules/Conch::Route::Validation.md index 9b24e6673..c985f5a7b 100644 --- a/docs/modules/Conch::Route::Validation.md +++ b/docs/modules/Conch::Route::Validation.md @@ -6,31 +6,33 @@ Conch::Route::Validation ## routes -Sets up the routes for /validation, /validation\_plan and /validation\_state: +Sets up the routes for /validation, /validation\_plan and /validation\_state. + +# ROUTE ENDPOINTS All routes require authentication. -### `GET /validation` +## `GET /validation` - Response: [response.json#/definitions/Validations](../json-schema/response.json#/definitions/Validations) -### `GET /validation/:validation_id_or_name` +## `GET /validation/:validation_id_or_name` - Response: [response.json#/definitions/Validation](../json-schema/response.json#/definitions/Validation) -### `GET /validation_plan` +## `GET /validation_plan` - Response: [response.json#/definitions/ValidationPlans](../json-schema/response.json#/definitions/ValidationPlans) -### `GET /validation_plan/:validation_plan_id_or_name` +## `GET /validation_plan/:validation_plan_id_or_name` - Response: [response.json#/definitions/ValidationPlan](../json-schema/response.json#/definitions/ValidationPlan) -### `GET /validation_plan/:validation_plan_id_or_name/validation` +## `GET /validation_plan/:validation_plan_id_or_name/validation` - Response: [response.json#/definitions/Validations](../json-schema/response.json#/definitions/Validations) -### `GET /validation_state/:validation_state_id` +## `GET /validation_state/:validation_state_id` - Response: [response.json#/definitions/ValidationStateWithResults](../json-schema/response.json#/definitions/ValidationStateWithResults) diff --git a/docs/modules/Conch::Route::Workspace.md b/docs/modules/Conch::Route::Workspace.md index f59e19983..d22884e4b 100644 --- a/docs/modules/Conch::Route::Workspace.md +++ b/docs/modules/Conch::Route::Workspace.md @@ -11,27 +11,29 @@ Sets up the routes for /workspace. Note that in all routes using `:workspace_id_or_name`, the stash for `workspace_id` will be populated, as well as `workspace_name` if the identifier was not a UUID. +# ROUTE ENDPOINTS + All routes require authentication. Users will require access to the workspace (or one of its ancestors) at a minimum [role](../modules/Conch%3A%3ADB%3A%3AResult%3A%3AUserWorkspaceRole#role), as indicated. -### `GET /workspace` +## `GET /workspace` - User requires the read-only role - Response: [response.json#/definitions/WorkspacesAndRoles](../json-schema/response.json#/definitions/WorkspacesAndRoles) -### `GET /workspace/:workspace_id_or_name` +## `GET /workspace/:workspace_id_or_name` - User requires the read-only role - Response: [response.json#/definitions/WorkspaceAndRole](../json-schema/response.json#/definitions/WorkspaceAndRole) -### `GET /workspace/:workspace_id_or_name/child` +## `GET /workspace/:workspace_id_or_name/child` - User requires the read-only role - Response: [response.json#/definitions/WorkspacesAndRoles](../json-schema/response.json#/definitions/WorkspacesAndRoles) -### `POST /workspace/:workspace_id_or_name/child?send_mail=<1|0>` +## `POST /workspace/:workspace_id_or_name/child?send_mail=<1|0>` Takes one optional query parameter `send_mail=<1|0>` (defaults to `1`) to send an email to the parent workspace admins. @@ -40,7 +42,7 @@ an email to the parent workspace admins. - Request: [request.json#/definitions/WorkspaceCreate](../json-schema/request.json#/definitions/WorkspaceCreate) - Response: [response.json#/definitions/WorkspaceAndRole](../json-schema/response.json#/definitions/WorkspaceAndRole) -### `GET /workspace/:workspace_id_or_name/device` +## `GET /workspace/:workspace_id_or_name/device` Accepts the following optional query parameters: @@ -52,28 +54,28 @@ Accepts the following optional query parameters: - User requires the read-only role - Response: [response.json#/definitions/Devices](../json-schema/response.json#/definitions/Devices), [response.json#/definitions/DeviceIds](../json-schema/response.json#/definitions/DeviceIds) or [response.json#/definitions/DeviceSerials](../json-schema/response.json#/definitions/DeviceSerials) -### `GET /workspace/:workspace_id_or_name/device/pxe` +## `GET /workspace/:workspace_id_or_name/device/pxe` - User requires the read-only role - Response: [response.json#/definitions/WorkspaceDevicePXEs](../json-schema/response.json#/definitions/WorkspaceDevicePXEs) -### `GET /workspace/:workspace_id_or_name/rack` +## `GET /workspace/:workspace_id_or_name/rack` - User requires the read-only role - Response: [response.json#/definitions/WorkspaceRackSummary](../json-schema/response.json#/definitions/WorkspaceRackSummary) -### `POST /workspace/:workspace_id_or_name/rack` +## `POST /workspace/:workspace_id_or_name/rack` - User requires the admin role - Request: [request.json#/definitions/WorkspaceAddRack](../json-schema/request.json#/definitions/WorkspaceAddRack) - Response: Redirect to the workspace's racks -### `DELETE /workspace/:workspace_id_or_name/rack/:rack_id_or_name` +## `DELETE /workspace/:workspace_id_or_name/rack/:rack_id_or_name` - User requires the admin role - Response: `204 NO CONTENT` -### `GET /workspace/:workspace_id_or_name/relay` +## `GET /workspace/:workspace_id_or_name/relay` Takes one query optional parameter, `?active_minutes=X` to constrain results to those updated with in the last `X` minutes. @@ -81,17 +83,17 @@ those updated with in the last `X` minutes. - User requires the read-only role - Response: [response.json#/definitions/WorkspaceRelays](../json-schema/response.json#/definitions/WorkspaceRelays) -### `GET /workspace/:workspace_id_or_name/relay/:relay_id/device` +## `GET /workspace/:workspace_id_or_name/relay/:relay_id/device` - User requires the read-only role - Response: [response.json#/definitions/Devices](../json-schema/response.json#/definitions/Devices) -### `GET /workspace/:workspace_id_or_name/user` +## `GET /workspace/:workspace_id_or_name/user` - User requires the admin role - Response: [response.json#/definitions/WorkspaceUsers](../json-schema/response.json#/definitions/WorkspaceUsers) -### `POST /workspace/:workspace_id_or_name/user?send_mail=<1|0>` +## `POST /workspace/:workspace_id_or_name/user?send_mail=<1|0>` Takes one optional query parameter `send_mail=<1|0>` (defaults to `1`) to send an email to the user and workspace admins. @@ -100,7 +102,7 @@ an email to the user and workspace admins. - Request: [request.json#/definitions/WorkspaceAddUser](../json-schema/request.json#/definitions/WorkspaceAddUser) - Response: `204 NO CONTENT` -### `DELETE /workspace/:workspace_id_or_name/user/:target_user_id_or_email?send_mail=<1|0>` +## `DELETE /workspace/:workspace_id_or_name/user/:target_user_id_or_email?send_mail=<1|0>` Takes one optional query parameter `send_mail=<1|0>` (defaults to `1`) to send an email to the user and workspace admins. @@ -108,12 +110,12 @@ an email to the user and workspace admins. - User requires the admin role - Response: `204 NO CONTENT` -### `GET /workspace/:workspace_id_or_name/organization` +## `GET /workspace/:workspace_id_or_name/organization` - User requires the admin role - Response: [response.json#/definitions/WorkspaceOrganizations](../json-schema/response.json#/definitions/WorkspaceOrganizations) -### `POST /workspace/:workspace_id_or_name/organization?send_mail=<1|0>` +## `POST /workspace/:workspace_id_or_name/organization?send_mail=<1|0>` Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the organization members and workspace admins. @@ -122,7 +124,7 @@ an email to the organization members and workspace admins. - Request: [request.json#/definitions/WorkspaceAddOrganization](../json-schema/request.json#/definitions/WorkspaceAddOrganization) - Response: `204 NO CONTENT` -### `DELETE /workspace/:workspace_id_or_name/organization/:organization_id_or_name?send_mail=<1|0>` +## `DELETE /workspace/:workspace_id_or_name/organization/:organization_id_or_name?send_mail=<1|0>` Takes one optional query parameter `send_mail=<1|0>` (defaults to 1) to send an email to the organization members and workspace admins. diff --git a/docs/modules/Test::Conch.md b/docs/modules/Test::Conch.md index 79423c233..8755088c7 100644 --- a/docs/modules/Test::Conch.md +++ b/docs/modules/Test::Conch.md @@ -176,8 +176,8 @@ See ["\_generate\_definition" in Test::Conch::Fixtures](../modules/Test%3A%3ACon Authenticates a user in the current test instance. Uses default (superuser) credentials if not provided. Optionally will bail out of **all** tests on failure. -This will set 'user' in the session (`$t->app->session('user')`), so a token is not needed -on subsequent requests. +This will set 'user' in the session (`$t->ua->cookie_jar`, accessed internally via +`$c->session('user_id')`), so a token is not needed on subsequent requests. ## txn\_local diff --git a/json-schema/request.yaml b/json-schema/request.yaml index 11e842d55..95a529b2f 100644 --- a/json-schema/request.yaml +++ b/json-schema/request.yaml @@ -369,6 +369,22 @@ definitions: $ref: common.yaml#/definitions/email_address password: $ref: common.yaml#/definitions/non_empty_string + set_session: + type: boolean + default: false + UserIdOrEmail: + type: object + additionalProperties: true + oneOf: + - required: + - user_id + - required: + - email + properties: + user_id: + $ref: common.yaml#/definitions/uuid + email: + $ref: common.yaml#/definitions/email_address UserPassword: type: object additionalProperties: false diff --git a/lib/Conch.pm b/lib/Conch.pm index cdea751a5..a6f9f5ce9 100644 --- a/lib/Conch.pm +++ b/lib/Conch.pm @@ -35,11 +35,13 @@ and therefore other helpers). sub startup { my $self = shift; - # Configuration - $self->plugin('Config'); - $self->secrets($self->config('secrets')); $self->sessions->cookie_name('conch'); - $self->sessions->default_expiration(2592000); # 30 days + $self->sessions->default_expiration(86400); # 1 day + $self->sessions->samesite('Strict'); # do not send with cross-site requests + $self->sessions->secure(1) if $ENV{MOJO_MODE} eq 'production'; # https only + + $self->plugin('Config'); + $self->secrets(delete $self->config->{secrets}); $self->plugin('Conch::Plugin::Features', $self->config); $self->plugin('Conch::Plugin::Logging', $self->config); @@ -65,7 +67,7 @@ sub startup { if (ref $args->{json} eq 'ARRAY' # TODO: skip if ?page_size is passed (and we actually used it). - and $args->{json}->@* >= ($c->app->config->{rollbar}{warn_payload_elements} // 35) + and $args->{json}->@* >= (($c->app->config('rollbar')//{})->{warn_payload_elements} // 35) and $c->feature('rollbar')) { my $endpoint = join '#', map $_//'', ($c->match->stack->[-1]//{})->@{qw(controller action)}; $c->send_message_to_rollbar( @@ -79,7 +81,7 @@ sub startup { # do this after the response has been sent $c->on(finish => sub ($c) { my $body_size = $c->res->body_size; - if ($body_size >= ($c->app->config->{rollbar}{warn_payload_size} // 10000) + if ($body_size >= (($c->app->config('rollbar')//{})->{warn_payload_size} // 10000) and $c->feature('rollbar')) { my $endpoint = join '#', map $_//'', ($c->match->stack->[-1]//{})->@{qw(controller action)}; $c->send_message_to_rollbar( diff --git a/lib/Conch/Command/copy_user_data.pm b/lib/Conch/Command/copy_user_data.pm index 1c2f4f9fe..10801d95e 100644 --- a/lib/Conch/Command/copy_user_data.pm +++ b/lib/Conch/Command/copy_user_data.pm @@ -58,7 +58,7 @@ sub run ($self, @opts) { my $app = $self->app; my $app_name = join(' ', $app->moniker, 'copy_user_data', $app->version_tag, '('.$$.')'); - my $db_credentials = Conch::DB::Util::get_credentials($app->config->{database}, $app->log); + my $db_credentials = Conch::DB::Util::get_credentials($app->config('database'), $app->log); my ($from_schema, $to_schema) = map Conch::DB->connect( $db_credentials->{dsn} =~ s/(?<=dbi:Pg:dbname=)([^;]+)(?=;host=)/$_/r, diff --git a/lib/Conch/Command/create_token.pm b/lib/Conch/Command/create_token.pm index 33ea20d25..a7e1b87a5 100644 --- a/lib/Conch/Command/create_token.pm +++ b/lib/Conch/Command/create_token.pm @@ -39,7 +39,7 @@ sub run ($self, @opts) { my $user = $self->app->db_user_accounts->active->find_by_email($opt->email); die 'cannot find user with email ', $opt->email if not $user; - my $expires_abs = time + (($self->app->config('jwt') || {})->{custom_token_expiry} // 86400*365*5); + my $expires_abs = time + (($self->app->config('authentication')//{})->{custom_token_expiry} // 86400*365*5); my ($token, $jwt) = $self->app->generate_jwt($user->id, $expires_abs, $opt->name); say $jwt; } diff --git a/lib/Conch/Controller/Build.pm b/lib/Conch/Controller/Build.pm index bbc0ac4aa..3318f19db 100644 --- a/lib/Conch/Controller/Build.pm +++ b/lib/Conch/Controller/Build.pm @@ -278,16 +278,7 @@ sub add_user ($c) { my $input = $c->validate_request('BuildAddUser'); return if not $input; - my $user_rs = $c->db_user_accounts->active; - my $user = $input->{user_id} ? $user_rs->find($input->{user_id}) - : $input->{email} ? $user_rs->find_by_email($input->{email}) - : undef; - if (not $user) { - $c->log->debug('Could not find user '.$input->@{qw(user_id email)}); - return $c->status(404); - } - - $c->stash('target_user', $user); + my $user = $c->stash('target_user'); my $build_name = $c->stash('build_name') // $c->stash('build_rs')->get_column('name')->single; # check if the user already has access to this build diff --git a/lib/Conch/Controller/Device.pm b/lib/Conch/Controller/Device.pm index 958c23ea7..5f2953eb4 100644 --- a/lib/Conch/Controller/Device.pm +++ b/lib/Conch/Controller/Device.pm @@ -135,8 +135,7 @@ reflected in the checksum. sub get ($c) { # allow the (authenticated) client to cache the result based on updated time my $etag = Digest::MD5::md5_hex($c->stash('device_rs')->get_column('updated')->single); - # TODO: this is really a weak etag. requires https://github.com/mojolicious/mojo/pull/1420 - return $c->status(304) if $c->is_fresh(etag => $etag); + return $c->status(304) if $c->is_fresh(etag => qq{W/"$etag"}); my $device = $c->stash('device_rs')->with_sku->with_build_name->single; my $latest_report = $c->stash('device_rs')->latest_device_report->get_column('report')->single; diff --git a/lib/Conch/Controller/Login.pm b/lib/Conch/Controller/Login.pm index 89ef603aa..f4ef56641 100644 --- a/lib/Conch/Controller/Login.pm +++ b/lib/Conch/Controller/Login.pm @@ -21,26 +21,17 @@ Create a response containing a login JWT, which the user should later present in =cut -sub _respond_with_jwt ($c, $user_id, $expires_delta = undef) { - my $jwt_config = $c->app->config('jwt') || {}; - - my $expires_abs = time + ( - defined $expires_delta ? $expires_delta - # system admin default: 30 days - : $c->is_system_admin ? ($jwt_config->{system_admin_expiry} || 2592000) - # normal default: 1 day - : ($jwt_config->{normal_expiry} || 86400)); - +sub _respond_with_jwt ($c, $user_id, $expires_epoch) { my ($session_token, $jwt) = $c->generate_jwt( $user_id, - $expires_abs, + $expires_epoch, 'login_jwt_'.join('_', Time::HiRes::gettimeofday), # reasonably unique name ); return if $c->res->code; - $c->res->headers->last_modified(Mojo::Date->new($session_token->created->epoch)); - $c->res->headers->expires(Mojo::Date->new($session_token->expires->epoch)); + $c->res->headers->last_modified(Mojo::Date->new(time)); + $c->res->headers->expires(Mojo::Date->new($expires_epoch)); return $c->status(200, { jwt_token => $jwt }); } @@ -48,9 +39,8 @@ sub _respond_with_jwt ($c, $user_id, $expires_delta = undef) { Handle the details of authenticating the user, with one of the following options: - * existing session for the user * signed JWT in the Authorization Bearer header - * Old 'conch' session cookie + * existing session for the user (using the 'conch' session cookie) Does not terminate the connection if authentication is successful, allowing for chaining to subsequent routes and actions. @@ -108,11 +98,14 @@ sub authenticate ($c) { $c->stash('token_id', $jwt_claims->{token_id}); } - if ($c->session('user')) { + if ($c->session('user_id')) { return $c->status(401, { error => 'user session is invalid' }) - if not is_uuid($c->session('user')) or ($user_id and $c->session('user') ne $user_id); - $c->log->debug('using session user='.$c->session('user')); - $user_id ||= $c->session('user'); + if not is_uuid($c->session('user_id')) or ($user_id and $c->session('user_id') ne $user_id); + + if (not $user_id) { + $user_id = $c->session('user_id'); + $c->log->debug('using session user_id='.$user_id); + } } # clear out all expired session tokens @@ -125,26 +118,26 @@ sub authenticate ($c) { # api tokens are exempt from this check if ((not $session_token or $session_token->is_login) - and $user->refuse_session_auth) { - if ($user->force_password_change) { - if ($c->req->url ne '/user/me/password') { - $c->log->debug('attempt to authenticate before changing insecure password'); - - # ensure session and and all login JWTs expire in no more than 10 minutes - $c->session(expiration => 10 * 60); - $user->user_session_tokens->login_only - ->update({ expires => \'least(expires, now() + interval \'10 minutes\')' }) if $session_token; - - $c->res->headers->location($c->url_for('/user/me/password')); - return $c->status(401); - } - } - else { - $c->log->debug('user\'s tokens were revoked - they must /login again'); - return $c->status(401); - } + and $user->force_password_change + and $c->req->url ne '/user/me/password' + ) { + $c->log->debug('attempt to authenticate before changing insecure password'); + + # ensure session and all login JWTs expire in no more than 10 minutes + $c->_update_session($c->session('user_id'), time + 10 * 60); + $user->user_session_tokens->login_only + ->update({ expires => \'least(expires, now() + interval \'10 minutes\')' }) if $session_token; + + $c->res->headers->location($c->url_for('/user/me/password')); + return $c->status(401); + } + + if (not $session_token and $user->refuse_session_auth) { + $c->log->debug('user attempting to authenticate with session, but refuse_session_auth is set'); + return $c->status(401); } + # the gauntlet has been successfully run! $c->stash('user_id', $user_id); $c->stash('user', $user); return 1; @@ -186,8 +179,6 @@ sub login ($c) { $c->stash('user_id', $user->id); $c->stash('user', $user); - $c->session(user => $user->id) if not $c->feature('stop_conch_cookie_issue'); - # clear out all expired session tokens $c->db_user_session_tokens->expired->delete; @@ -199,11 +190,12 @@ sub login ($c) { password => Authen::Passphrase::RejectAll->new, # ensure password cannot be used again }); # password must be reset within 10 minutes - $c->session(expires => time + 10 * 60); + + $c->_update_session($user->id, $input->{set_session} ? time + 10 * 60 : 0); # we logged the user in, but he must now change his password (within 10 minutes) $c->res->headers->location($c->url_for('/user/me/password')); - return $c->_respond_with_jwt($user->id, 10 * 60); + return $c->_respond_with_jwt($user->id, time + 10 * 60); } $c->log->info('user '.$user->name.' ('.$user->email.') logged in'); @@ -225,20 +217,30 @@ sub login ($c) { if (my $token = $token_rs->order_by({ -desc => 'created' })->rows(1)->single) { $c->res->headers->last_modified(Mojo::Date->new($token->created->epoch)); $c->res->headers->expires(Mojo::Date->new($token->expires->epoch)); + + $c->_update_session($user->id, $input->{set_session} ? $token->expires->epoch : 0); + return $c->status(200, { jwt_token => $c->generate_jwt_from_token($token) }); } - return $c->_respond_with_jwt($user->id); + my $config = $c->app->config('authentication') // {}; + my $expires_epoch = time + + ($c->is_system_admin ? ($config->{system_admin_expiry} || 2592000) # 30 days + : ($config->{normal_expiry} || 86400)); # 1 day + + $c->_update_session($user->id, $input->{set_session} ? $expires_epoch : 0); + + return $c->_respond_with_jwt($user->id, $expires_epoch); } =head2 logout -Logs a user out by expiring their session +Logs a user out by expiring their JWT and user session =cut sub logout ($c) { - $c->session(expires => 1); + $c->_update_session; # expire this user's token # (assuming we have the user's id, which we probably don't) @@ -257,7 +259,7 @@ sub logout ($c) { =head2 refresh_token -Refresh a user's JWT token. Deletes the old token and expires the session. +Refresh a user's JWT token and persistent user session, deleting the old token. =cut @@ -265,8 +267,6 @@ sub refresh_token ($c) { $c->validate_request('Null'); return if $c->res->code; - $c->session('expires', 1) if $c->session('user'); - $c->db_user_session_tokens ->search({ id => $c->stash('token_id'), user_id => $c->stash('user_id') }) ->unexpired->expire @@ -275,7 +275,25 @@ sub refresh_token ($c) { # clear out all expired session tokens $c->db_user_session_tokens->expired->delete; - return $c->_respond_with_jwt($c->stash('user_id')); + my $config = $c->app->config('authentication') // {}; + my $expires_epoch = time + + ($c->is_system_admin ? ($config->{system_admin_expiry} || 2592000) # 30 days + : ($config->{normal_expiry} || 86400)); # 1 day + + # renew the session, if there is one + $c->_update_session($c->stash('user_id'), $expires_epoch); + + return $c->_respond_with_jwt($c->stash('user_id'), $expires_epoch); +} + +sub _update_session ($c, $user_id = undef, $expires_epoch = 0) { + if (not $user_id or not $expires_epoch or $c->feature('stop_conch_cookie_issue')) { + $c->session('expires', 1); + } + else { + $c->session('user_id', $user_id); + $c->session('expires', $expires_epoch); + } } 1; diff --git a/lib/Conch/Controller/Organization.pm b/lib/Conch/Controller/Organization.pm index f37406dc5..96dd3982d 100644 --- a/lib/Conch/Controller/Organization.pm +++ b/lib/Conch/Controller/Organization.pm @@ -213,16 +213,7 @@ sub add_user ($c) { my $input = $c->validate_request('OrganizationAddUser'); return if not $input; - my $user_rs = $c->db_user_accounts->active; - my $user = $input->{user_id} ? $user_rs->find($input->{user_id}) - : $input->{email} ? $user_rs->find_by_email($input->{email}) - : undef; - if (not $user) { - $c->log->debug('Could not find user '.$input->@{qw(user_id email)}); - return $c->status(404); - } - - $c->stash('target_user', $user); + my $user = $c->stash('target_user'); my $organization_name = $c->stash('organization_name') // $c->stash('organization_rs')->get_column('name')->single; # check if the user already has access to this organization diff --git a/lib/Conch/Controller/User.pm b/lib/Conch/Controller/User.pm index 0337c0927..259df11a0 100644 --- a/lib/Conch/Controller/User.pm +++ b/lib/Conch/Controller/User.pm @@ -40,7 +40,7 @@ sub find_user ($c) { if ($user->deactivated) { return $c->status(410, { error => 'user is deactivated', - user => { map +($_ => $user->$_), qw(id email name created deactivated) }, + $c->is_system_admin ? ( user => { map +($_ => $user->$_), qw(id email name created deactivated) } ) : (), }); } @@ -385,6 +385,8 @@ sub update ($c) { return $c->status(400, { error => 'user email "'.$input->{email}.'" is not a valid RFC822 address' }) if exists $input->{email} and not Email::Valid->address($input->{email}); + my $is_system_admin = $c->is_system_admin; + my $user = $c->stash('target_user'); my %orig_columns = $user->get_columns; $user->set_columns($input); @@ -392,19 +394,16 @@ sub update ($c) { return $c->status(204) if not keys %dirty_columns; - if (exists $dirty_columns{email} and fc $input->{email} ne fc $orig_columns{email} - and my $dupe_user = $c->db_user_accounts->active->find_by_email($input->{email})) { - return $c->status(409, { - error => 'duplicate user found', - user => { map +($_ => $dupe_user->$_), qw(id email name created deactivated) }, - }); - } + return $c->status(403) if $dirty_columns{is_admin} and not $is_system_admin; - if (exists $dirty_columns{name} - and my $dupe_user = $c->db_user_accounts->active->search({ name => $input->{name} })->single) { + if (my $dupe_user = + (exists $dirty_columns{email} && (fc $input->{email} ne fc $orig_columns{email}) + && $c->db_user_accounts->active->find_by_email($input->{email})) + || (exists $dirty_columns{name} + && $c->db_user_accounts->active->search({ name => $input->{name} })->single) ) { return $c->status(409, { error => 'duplicate user found', - user => { map +($_ => $dupe_user->$_), qw(id email name created deactivated) }, + $is_system_admin ? ( user => { map +($_ => $dupe_user->$_), qw(id email name created deactivated) } ) : (), }); } @@ -488,6 +487,7 @@ sub create ($c) { $input->{is_admin} = ($input->{is_admin} ? 1 : 0); # password will be hashed in constructor + # FIXME: should set force_password_change - see GH#975 my $user = $c->db_user_accounts->create($input); $c->log->info('created user: '.$user->name.', email: '.$user->email.', id: '.$user->id); @@ -611,7 +611,7 @@ sub create_api_token ($c) { my $user = $c->stash('target_user'); # default expiration: 5 years - my $expires_abs = time + (($c->app->config('jwt') || {})->{custom_token_expiry} // 86400*365*5); + my $expires_abs = time + (($c->app->config('authentication')//{})->{custom_token_expiry} // 86400*365*5); my ($token, $jwt) = $c->generate_jwt($user->id, $expires_abs, $input->{name}); return if $c->res->code; diff --git a/lib/Conch/Controller/WorkspaceDevice.pm b/lib/Conch/Controller/WorkspaceDevice.pm index e950ec5d3..e2336c982 100644 --- a/lib/Conch/Controller/WorkspaceDevice.pm +++ b/lib/Conch/Controller/WorkspaceDevice.pm @@ -130,9 +130,9 @@ sub device_totals ($c) { } return $c->reply->not_found if not $workspace; - my %switch_aliases = map +($_ => 1), $c->app->config->{switch_aliases}->@*; - my %storage_aliases = map +($_ => 1), $c->app->config->{storage_aliases}->@*; - my %compute_aliases = map +($_ => 1), $c->app->config->{compute_aliases}->@*; + my %switch_aliases = map +($_ => 1), ($c->app->config('switch_aliases')//{})->@*; + my %storage_aliases = map +($_ => 1), ($c->app->config('storage_aliases')//{})->@*; + my %compute_aliases = map +($_ => 1), ($c->app->config('compute_aliases')//{})->@*; my @counts = $workspace ->related_resultset('workspace_racks') diff --git a/lib/Conch/Controller/WorkspaceUser.pm b/lib/Conch/Controller/WorkspaceUser.pm index 707b237b2..9933617c2 100644 --- a/lib/Conch/Controller/WorkspaceUser.pm +++ b/lib/Conch/Controller/WorkspaceUser.pm @@ -68,15 +68,10 @@ sub add_user ($c) { my $input = $c->validate_request('WorkspaceAddUser'); return if not $input; - my $user_rs = $c->db_user_accounts->active; - my $user = $input->{user_id} ? $user_rs->find($input->{user_id}) - : $input->{email} ? $user_rs->find_by_email($input->{email}) - : undef; - return $c->status(404) if not $user; + my $user = $c->stash('target_user'); return $c->status(204) if $user->is_admin; - $c->stash('target_user', $user); my $workspace_id = $c->stash('workspace_id'); # check if the user already has access to this workspace (whether directly or through a diff --git a/lib/Conch/DB/Result/ValidationResult.pm b/lib/Conch/DB/Result/ValidationResult.pm index cfd2e8a55..86adf5851 100644 --- a/lib/Conch/DB/Result/ValidationResult.pm +++ b/lib/Conch/DB/Result/ValidationResult.pm @@ -143,6 +143,46 @@ __PACKAGE__->add_columns( __PACKAGE__->set_primary_key("id"); +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=item * L + +=item * L + +=item * L + +=item * L + +=item * L + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint( + "validation_result_all_columns_key", + [ + "device_id", + "hardware_product_id", + "validation_id", + "message", + "hint", + "status", + "category", + "component", + ], +); + =head1 RELATIONS =head2 device @@ -221,7 +261,7 @@ __PACKAGE__->many_to_many( # Created by DBIx::Class::Schema::Loader v0.07049 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+l8wIgJwgE/xE72CF/Dr4Q +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:cdTosC+PxDTHDZw4mNjetQ __PACKAGE__->add_columns( '+created' => { is_serializable => 0 }, diff --git a/lib/Conch/Plugin/DeprecatedAction.pm b/lib/Conch/Plugin/DeprecatedAction.pm index e18648e5b..a633b7fe0 100644 --- a/lib/Conch/Plugin/DeprecatedAction.pm +++ b/lib/Conch/Plugin/DeprecatedAction.pm @@ -19,7 +19,7 @@ Mojo plugin to detect and report the usage of deprecated controller actions. Sets the C header in the response. -Also sends a message to rollbar when a deprecated action is invoked, if the +Also sends a message to Rollbar when a deprecated action is invoked, if the C feature is enabled. =cut diff --git a/lib/Conch/Plugin/Rollbar.pm b/lib/Conch/Plugin/Rollbar.pm index 3d4fef41d..62b257b53 100644 --- a/lib/Conch/Plugin/Rollbar.pm +++ b/lib/Conch/Plugin/Rollbar.pm @@ -39,8 +39,7 @@ sub register ($self, $app, $config) { $app->hook(before_render => sub ($c, $args) { my $template = $args->{template}; - if (my $exception = $c->stash('exception') - or ($template and $template =~ /exception/)) { + if (my $exception = $c->stash('exception')) { my $rollbar_id = $c->send_exception_to_rollbar($exception); $c->log->debug('exception sent to rollbar: id '.$rollbar_id) if $rollbar_id; } @@ -100,7 +99,7 @@ thus created. my $notifier; $app->helper(send_exception_to_rollbar => sub ($c, $exception) { - $notifier //= _create_notifier($c->app, $c->config); + $notifier //= _create_notifier($c->app, $c->app->config); return if not $notifier; my @frames = map +{ @@ -160,7 +159,7 @@ A string or data structure of fingerprint data for grouping occurrences is optio Carp::croak('severity must be one of: '.join(', ',@message_levels)) if !$ENV{MOJO_MODE} and none { $severity eq $_ } @message_levels; - $notifier //= _create_notifier($c->app, $c->config); + $notifier //= _create_notifier($c->app, $c->app->config); return if not $notifier; my $rollbar_id = create_uuid_str(); diff --git a/lib/Conch/Route.pm b/lib/Conch/Route.pm index 50aab7689..c93cc2039 100644 --- a/lib/Conch/Route.pm +++ b/lib/Conch/Route.pm @@ -46,9 +46,18 @@ sub all_routes ( $app, # the Conch app ) { - # provides a route to chain to that first checks the user is a system admin. +=head1 SHORTCUTS + +These are available on the root router. See L. + +=head2 require_system_admin + +Chainable route that aborts with HTTP 403 if the user is not a system admin. + +=cut + $root->add_shortcut(require_system_admin => sub ($r) { - $r->any(sub ($c) { + $r->under('/', sub ($c) { return $c->status(401) if not $c->stash('user') or not $c->stash('user_id'); @@ -58,7 +67,26 @@ sub all_routes ( } return 1; - })->under; + }); + }); + +=head2 find_user_from_payload + +Chainable route that looks up the user by C or C in the JSON payload, +aborting with HTTP 410 or HTTP 404 if not found. + +=cut + + # provides a route to chain to that looks up the user provided in the payload + $root->add_shortcut(find_user_from_payload => sub ($r) { + $r->under('/', sub ($c) { + my $input = $c->validate_request('UserIdOrEmail'); + return if not $input; + + $c->stash('target_user_id_or_email', $input->{user_id} // $input->{email}); + return 1; + }) + ->under('/')->to('user#find_user'); }); # allow routes to be specified as, e.g. ->get('/')->to(...) @@ -132,26 +160,28 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + Unless otherwise specified, all routes require authentication. Full access is granted to system admin users, regardless of workspace, build or other role entries. -Successful (http 2xx code) response structures are as described for each endpoint. +Successful (HTTP 2xx code) response structures are as described for each endpoint. Error responses will use: =over -=item * failure to validate query parameters: http 400, F +=item * failure to validate query parameters: HTTP 400, F -=item * failure to validate request body payload: http 400, F +=item * failure to validate request body payload: HTTP 400, F -=item * all other errors, unless specified: http 4xx, F +=item * all other errors, unless specified: HTTP 4xx, F =back -=head3 C +=head2 C =over 4 @@ -161,7 +191,7 @@ Error responses will use: =back -=head3 C +=head2 C =over 4 @@ -171,7 +201,7 @@ Error responses will use: =back -=head3 C +=head2 C =over 4 @@ -181,7 +211,7 @@ Error responses will use: =back -=head3 C +=head2 C =over 4 @@ -191,9 +221,9 @@ Error responses will use: =back -=head3 C +=head2 C -=head3 C +=head2 C =over 4 @@ -205,7 +235,7 @@ Error responses will use: =back -=head3 C +=head2 C =over 4 @@ -215,47 +245,47 @@ Error responses will use: =back -=head3 C<* /dc>, C<* /room>, C<* /rack_role>, C<* /rack>, C<* /layout> +=head2 C<* /dc>, C<* /room>, C<* /rack_role>, C<* /rack>, C<* /layout> See L -=head3 C<* /device> +=head2 C<* /device> See L -=head3 C<* /device_report> +=head2 C<* /device_report> See L -=head3 C<* /hardware_product> +=head2 C<* /hardware_product> See L -=head3 C<* /hardware_vendor> +=head2 C<* /hardware_vendor> See L -=head3 C<* /organization> +=head2 C<* /organization> See L -=head3 C<* /relay> +=head2 C<* /relay> See L -=head3 C<* /schema> +=head2 C<* /schema> See L -=head3 C<* /user> +=head2 C<* /user> See L -=head3 C<* /validation>, C<* /validation_plan>, C<* /validation_state> +=head2 C<* /validation>, C<* /validation_plan>, C<* /validation_state> See L -=head3 C<* /workspace> +=head2 C<* /workspace> See L diff --git a/lib/Conch/Route/Build.pm b/lib/Conch/Route/Build.pm index 1a3022049..4a260826d 100644 --- a/lib/Conch/Route/Build.pm +++ b/lib/Conch/Route/Build.pm @@ -50,7 +50,7 @@ sub routes { $with_build_admin->get('/user')->to('#get_users'); # POST /build/:build_id_or_name/user?send_mail=<1|0> - $with_build_admin->post('/user')->to('#add_user'); + $with_build_admin->find_user_from_payload->post('/user')->to('build#add_user'); # DELETE /build/:build_id_or_name/user/#target_user_id_or_email?send_mail=<1|0> $with_build_admin @@ -108,9 +108,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C Supports the following optional query parameters: @@ -130,7 +132,7 @@ Supports the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -142,7 +144,7 @@ Supports the following optional query parameters: =back -=head3 C +=head2 C Supports the following optional query parameters: @@ -164,7 +166,7 @@ Supports the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -176,7 +178,7 @@ Supports the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -186,7 +188,7 @@ Supports the following optional query parameters: =back -=head3 C> +=head2 C> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the user. @@ -201,7 +203,7 @@ an email to the user. =back -=head3 C> +=head2 C> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the user. @@ -214,7 +216,7 @@ an email to the user. =back -=head3 C +=head2 C =over 4 @@ -224,7 +226,7 @@ an email to the user. =back -=head3 C<< POST /build/:build_id_or_name/organization?send_mail=<1|0> >> +=head2 C<< POST /build/:build_id_or_name/organization?send_mail=<1|0> >> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the organization members and build admins. @@ -239,7 +241,7 @@ an email to the organization members and build admins. =back -=head3 C<< DELETE /build/:build_id_or_name/organization/:organization_id_or_name?send_mail=<1|0> >> +=head2 C<< DELETE /build/:build_id_or_name/organization/:organization_id_or_name?send_mail=<1|0> >> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the organization members and build admins. @@ -252,7 +254,7 @@ an email to the organization members and build admins. =back -=head3 C +=head2 C Accepts the following optional query parameters: @@ -275,7 +277,7 @@ Accepts the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -285,7 +287,7 @@ Accepts the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -299,7 +301,7 @@ L) =back -=head3 C +=head2 C =over 4 @@ -310,7 +312,7 @@ read-write role on the device (via a workspace or build; see L +=head2 C =over 4 @@ -320,7 +322,7 @@ read-write role on the device (via a workspace or build; see L +=head2 C =over 4 @@ -330,7 +332,7 @@ read-write role on the device (via a workspace or build; see L +=head2 C =over 4 diff --git a/lib/Conch/Route/Datacenter.pm b/lib/Conch/Route/Datacenter.pm index 0a1cf90db..f1709b544 100644 --- a/lib/Conch/Route/Datacenter.pm +++ b/lib/Conch/Route/Datacenter.pm @@ -12,7 +12,7 @@ Conch::Route::Datacenter =head2 routes -Sets up the routes for /dc: +Sets up the routes for /dc. =cut @@ -44,9 +44,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -56,7 +58,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -68,7 +70,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -78,7 +80,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -90,7 +92,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -100,7 +102,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -110,7 +112,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/DatacenterRoom.pm b/lib/Conch/Route/DatacenterRoom.pm index 159558004..b343a5bd0 100644 --- a/lib/Conch/Route/DatacenterRoom.pm +++ b/lib/Conch/Route/DatacenterRoom.pm @@ -12,7 +12,7 @@ Conch::Route::DatacenterRoom =head2 routes -Sets up the routes for /room: +Sets up the routes for /room. =cut @@ -72,7 +72,9 @@ __END__ All routes require authentication. -=head3 C +=head1 ROUTE ENDPOINTS + +=head2 C =over 4 @@ -82,7 +84,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -94,7 +96,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -105,7 +107,7 @@ the room =back -=head3 C +=head2 C =over 4 @@ -117,7 +119,7 @@ the room =back -=head3 C +=head2 C =over 4 @@ -127,7 +129,7 @@ the room =back -=head3 C +=head2 C =over 4 @@ -138,7 +140,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C =over 4 @@ -148,7 +150,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C =over 4 @@ -160,7 +162,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C =over 4 @@ -170,7 +172,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C =over 4 @@ -180,7 +182,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C =over 4 @@ -192,7 +194,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C =over 4 @@ -202,7 +204,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C =over 4 @@ -214,7 +216,7 @@ the room (in which case data returned is restricted to those racks) =back -=head3 C +=head2 C This method requires a request body. @@ -228,7 +230,7 @@ This method requires a request body. =back -=head3 C<< POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/phase?rack_only=<0|1> >> +=head2 C<< POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/phase?rack_only=<0|1> >> The query parameter C (defaults to C<0>) specifies whether to update only the rack's phase, or all the rack's devices' phases as well. @@ -243,15 +245,15 @@ only the rack's phase, or all the rack's devices' phases as well. =back -=head3 C +=head2 C See L>. -=head3 C +=head2 C See L>. -=head3 C +=head2 C See L>. diff --git a/lib/Conch/Route/Device.pm b/lib/Conch/Route/Device.pm index d404ccabc..1b6f84338 100644 --- a/lib/Conch/Route/Device.pm +++ b/lib/Conch/Route/Device.pm @@ -12,7 +12,7 @@ Conch::Route::Device =head2 routes -Sets up the routes for /device: +Sets up the routes for /device. =cut @@ -121,6 +121,8 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. The user's role (required for most endpoints) is determined by the build the device is @@ -132,7 +134,7 @@ a L in that workspace). Full (admin-level) access is also granted to a device if a report was sent for that device using a relay that registered with that user's credentials. -=head3 C +=head2 C =over 4 @@ -142,7 +144,7 @@ using a relay that registered with that user's credentials. =back -=head3 C +=head2 C Supports the following query parameters: @@ -168,7 +170,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -178,7 +180,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -188,7 +190,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -198,7 +200,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -208,7 +210,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -220,7 +222,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -232,7 +234,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -244,7 +246,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -256,7 +258,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -266,7 +268,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -278,7 +280,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -288,7 +290,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -300,7 +302,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -310,7 +312,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -320,7 +322,7 @@ below. =back -=head3 C +=head2 C =over 4 @@ -333,7 +335,7 @@ settings that do not start with C. =back -=head3 C +=head2 C =over 4 @@ -343,7 +345,7 @@ settings that do not start with C. =back -=head3 C +=head2 C =over 4 @@ -356,7 +358,7 @@ settings that do not start with C. =back -=head3 C +=head2 C =over 4 @@ -367,7 +369,7 @@ otherwise. =back -=head3 C +=head2 C Does not store validation results. @@ -381,7 +383,7 @@ Does not store validation results. =back -=head3 C +=head2 C Does not store validation results. @@ -395,7 +397,7 @@ Does not store validation results. =back -=head3 C<< GET /device/:device_id_or_serial_number/validation_state?status=&status=... >> +=head2 C<< GET /device/:device_id_or_serial_number/validation_state?status=&status=... >> Accepts the query parameter C, indicating the desired status(es) to search for (one of C, C, C). Can be used more than once. @@ -408,7 +410,7 @@ to search for (one of C, C, C). Can be used more than once. =back -=head3 C +=head2 C =over 4 @@ -418,7 +420,7 @@ to search for (one of C, C, C). Can be used more than once. =back -=head3 C +=head2 C =over 4 @@ -428,7 +430,7 @@ to search for (one of C, C, C). Can be used more than once. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/DeviceReport.pm b/lib/Conch/Route/DeviceReport.pm index 3d0ff1aa4..ff25f1f2e 100644 --- a/lib/Conch/Route/DeviceReport.pm +++ b/lib/Conch/Route/DeviceReport.pm @@ -12,7 +12,7 @@ Conch::Route::DeviceReport =head2 routes -Sets up the routes for /device_report: +Sets up the routes for /device_report. =cut @@ -40,9 +40,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -52,7 +54,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/HardwareProduct.pm b/lib/Conch/Route/HardwareProduct.pm index 95fcb2418..c188332b7 100644 --- a/lib/Conch/Route/HardwareProduct.pm +++ b/lib/Conch/Route/HardwareProduct.pm @@ -12,7 +12,7 @@ Conch::Route::HardwareProduct =head2 routes -Sets up the routes for /hardware_product: +Sets up the routes for /hardware_product. =cut @@ -49,9 +49,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -59,7 +61,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -71,7 +73,7 @@ All routes require authentication. =back -=head3 C +=head2 C Identifiers accepted: C, C, C and C. @@ -81,7 +83,7 @@ Identifiers accepted: C, C, C and C. =back -=head3 C +=head2 C Identifiers accepted: C, C, C and C. @@ -95,7 +97,7 @@ Identifiers accepted: C, C, C and C. =back -=head3 C +=head2 C Identifiers accepted: C, C, C and C. diff --git a/lib/Conch/Route/HardwareVendor.pm b/lib/Conch/Route/HardwareVendor.pm index 0ce8f5ae5..3370e062c 100644 --- a/lib/Conch/Route/HardwareVendor.pm +++ b/lib/Conch/Route/HardwareVendor.pm @@ -12,7 +12,7 @@ Conch::Route::HardwareVendor =head2 routes -Sets up the routes for /hardware_vendor: +Sets up the routes for /hardware_vendor. =cut @@ -45,9 +45,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -55,7 +57,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -63,7 +65,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -73,7 +75,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/Organization.pm b/lib/Conch/Route/Organization.pm index 9e49d3868..dc8171403 100644 --- a/lib/Conch/Route/Organization.pm +++ b/lib/Conch/Route/Organization.pm @@ -44,7 +44,7 @@ sub routes { $with_organization->require_system_admin->delete('/')->to('#delete'); # POST /organization/:organization_id_or_name/user?send_mail=<1|0> - $with_organization->post('/user')->to('#add_user'); + $with_organization->find_user_from_payload->post('/user')->to('organization#add_user'); # DELETE /organization/:organization_id_or_name/user/#target_user_id_or_email?send_mail=<1|0> $with_organization->under('/user/#target_user_id_or_email')->to('user#find_user') @@ -57,9 +57,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -67,7 +69,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -79,7 +81,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -89,7 +91,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -101,7 +103,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -111,7 +113,7 @@ All routes require authentication. =back -=head3 C> +=head2 C> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the user. @@ -126,7 +128,7 @@ an email to the user. =back -=head3 C> +=head2 C> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the user. diff --git a/lib/Conch/Route/Rack.pm b/lib/Conch/Route/Rack.pm index 0ff2aaf81..0381ec3e9 100644 --- a/lib/Conch/Route/Rack.pm +++ b/lib/Conch/Route/Rack.pm @@ -12,7 +12,7 @@ Conch::Route::Rack =head2 routes -Sets up the routes for /rack: +Sets up the routes for /rack. =cut @@ -87,13 +87,15 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. Take note: All routes that reference a specific rack (prefix C) are also available under C as well as C. -=head3 C +=head2 C =over 4 @@ -105,7 +107,7 @@ C. =back -=head3 C +=head2 C =over 4 @@ -115,7 +117,7 @@ C. =back -=head3 C +=head2 C =over 4 @@ -127,7 +129,7 @@ C. =back -=head3 C +=head2 C =over 4 @@ -137,7 +139,7 @@ C. =back -=head3 C +=head2 C =over 4 @@ -147,7 +149,7 @@ C. =back -=head3 C +=head2 C =over 4 @@ -159,7 +161,7 @@ C. =back -=head3 C +=head2 C =over 4 @@ -169,7 +171,7 @@ C. =back -=head3 C +=head2 C =over 4 @@ -181,7 +183,7 @@ C. =back -=head3 C +=head2 C This method requires a request body. @@ -195,7 +197,7 @@ This method requires a request body. =back -=head3 C<< POST /rack/:rack_id_or_name/phase?rack_only=<0|1> >> +=head2 C<< POST /rack/:rack_id_or_name/phase?rack_only=<0|1> >> The query parameter C (defaults to C<0>) specifies whether to update only the rack's phase, or all the rack's devices' phases as well. @@ -210,15 +212,15 @@ only the rack's phase, or all the rack's devices' phases as well. =back -=head3 C +=head2 C See L>. -=head3 C +=head2 C See L>. -=head3 C +=head2 C See L>. diff --git a/lib/Conch/Route/RackLayout.pm b/lib/Conch/Route/RackLayout.pm index 3f881036f..bfe39859a 100644 --- a/lib/Conch/Route/RackLayout.pm +++ b/lib/Conch/Route/RackLayout.pm @@ -12,7 +12,7 @@ Conch::Route::RackLayout =head2 routes -Sets up the routes for /layout: +Sets up the routes for /layout. =cut @@ -55,9 +55,16 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +Take note: All routes that reference a specific rack layout (prefix C) are +also available under C as +well as +C. + +=head2 C =over 4 @@ -67,7 +74,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -79,7 +86,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -89,7 +96,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -101,7 +108,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/RackRole.pm b/lib/Conch/Route/RackRole.pm index dfed454e4..1f76df8e7 100644 --- a/lib/Conch/Route/RackRole.pm +++ b/lib/Conch/Route/RackRole.pm @@ -12,7 +12,7 @@ Conch::Route::RackRole =head2 routes -Sets up the routes for /rack_role: +Sets up the routes for /rack_role. =cut @@ -42,9 +42,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -52,7 +54,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -64,7 +66,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -72,7 +74,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -84,7 +86,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/Relay.pm b/lib/Conch/Route/Relay.pm index cd61abc65..836fad8ca 100644 --- a/lib/Conch/Route/Relay.pm +++ b/lib/Conch/Route/Relay.pm @@ -12,7 +12,7 @@ Conch::Route::Relay =head2 routes -Sets up the routes for /relay: +Sets up the routes for /relay. =cut @@ -42,9 +42,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -54,7 +56,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -64,7 +66,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/Schema.pm b/lib/Conch/Route/Schema.pm index d4e19730b..7df5836f5 100644 --- a/lib/Conch/Route/Schema.pm +++ b/lib/Conch/Route/Schema.pm @@ -34,11 +34,13 @@ __END__ =pod -=head3 C +=head1 ROUTE ENDPOINTS -=head3 C +=head2 C -=head3 C +=head2 C + +=head2 C Returns the schema specified by type and name. diff --git a/lib/Conch/Route/User.pm b/lib/Conch/Route/User.pm index a6191f37b..faf23a4ac 100644 --- a/lib/Conch/Route/User.pm +++ b/lib/Conch/Route/User.pm @@ -12,7 +12,7 @@ Conch::Route::User =head2 routes -Sets up the routes for /user: +Sets up the routes for /user. =cut @@ -32,6 +32,9 @@ sub routes { # GET /user/me $user_me->get('/')->to('#get'); + # POST /user/me?send_mail=<1|0> + $user_me->post('/')->to('#update'); + # POST /user/me/revoke?send_mail=<1|0>&login_only=<0|1>&api_only=<0|1> $user_me->post('/revoke')->to('#revoke_user_tokens'); @@ -126,9 +129,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -136,7 +141,23 @@ All routes require authentication. =back -=head3 C<< POST /user/me/revoke?send_mail=<1|0>&login_only=<0|1>&api_only=<0|1> >> +=head2 C<< POST /user/:target_user_id_or_email?send_mail=<1|0> >> + +Optionally take the query parameter C (defaults to C<1>) to send +an email telling the user their account was updated + +=over 4 + +=item * Request: F + +=item * Success Response: Redirect to the user that was updated + +=item * Error response on duplicate user: F (only if the +calling user is a system admin) + +=back + +=head2 C<< POST /user/me/revoke?send_mail=<1|0>&login_only=<0|1>&api_only=<0|1> >> Optionally accepts the following query parameters: @@ -161,7 +182,7 @@ C and C cannot both be C<1>. =back -=head3 C<< POST /user/me/password?clear_tokens= >> +=head2 C<< POST /user/me/password?clear_tokens= >> Optionally takes a query parameter C, to also revoke the session tokens for the user, forcing the user to log in again. Possible options are: @@ -188,7 +209,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -196,7 +217,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -206,7 +227,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -214,7 +235,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -224,7 +245,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -232,7 +253,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -240,7 +261,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -250,7 +271,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -258,7 +279,7 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 @@ -266,20 +287,20 @@ otherwise, the user is logged out. =back -=head3 C +=head2 C =over 4 -=item * Requires system admin authorization +=item * Requires system admin authorization (when updating a different account than one's own) =item * Response: F =back -=head3 C<< POST /user/:target_user_id_or_email?send_mail=<1|0> >> +=head2 C<< POST /user/:target_user_id_or_email?send_mail=<1|0> >> Optionally take the query parameter C (defaults to C<1>) to send -an email telling the user their tokens were revoked +an email telling the user their account was updated =over 4 @@ -289,11 +310,12 @@ an email telling the user their tokens were revoked =item * Success Response: Redirect to the user that was updated -=item * Error response on duplicate user: F +=item * Error response on duplicate user: F (only if the +calling user is a system admin) =back -=head3 C<< DELETE /user/:target_user_id_or_email?clear_tokens=<1|0> >> +=head2 C<< DELETE /user/:target_user_id_or_email?clear_tokens=<1|0> >> When a user is deleted, all role entries (workspace, build, organization) are removed and are unrecoverable. @@ -309,7 +331,7 @@ revoke all session tokens for the user forcing all tools to log in again. =back -=head3 C<< POST /user/:target_user_id_or_email/revoke?login_only=<0|1>&api_only=<0|1> >> +=head2 C<< POST /user/:target_user_id_or_email/revoke?login_only=<0|1>&api_only=<0|1> >> Optionally accepts the following query parameters: @@ -332,7 +354,7 @@ C and C cannot both be C<1>. =back -=head3 C<< DELETE /user/:target_user_id_or_email/password?clear_tokens=&send_mail=<1|0> >> +=head2 C<< DELETE /user/:target_user_id_or_email/password?clear_tokens=&send_mail=<1|0> >> Optionally accepts the following query parameters: @@ -362,7 +384,7 @@ Optionally accepts the following query parameters: =back -=head3 C +=head2 C =over 4 @@ -372,7 +394,7 @@ Optionally accepts the following query parameters: =back -=head3 C<< POST /user?send_mail=<1|0> >> +=head2 C<< POST /user?send_mail=<1|0> >> Optionally takes a query parameter, C (defaults to C<1>) to send an email to the user with the new password. @@ -389,7 +411,7 @@ email to the user with the new password. =back -=head3 C +=head2 C =over 4 @@ -399,7 +421,7 @@ email to the user with the new password. =back -=head3 C +=head2 C =over 4 @@ -409,7 +431,7 @@ email to the user with the new password. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/Validation.pm b/lib/Conch/Route/Validation.pm index a8de02bd8..fdc112dba 100644 --- a/lib/Conch/Route/Validation.pm +++ b/lib/Conch/Route/Validation.pm @@ -12,7 +12,7 @@ Conch::Route::Validation =head2 routes -Sets up the routes for /validation, /validation_plan and /validation_state: +Sets up the routes for /validation, /validation_plan and /validation_state. =cut @@ -66,9 +66,11 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. -=head3 C +=head2 C =over 4 @@ -76,7 +78,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -84,7 +86,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -92,7 +94,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -100,7 +102,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 @@ -108,7 +110,7 @@ All routes require authentication. =back -=head3 C +=head2 C =over 4 diff --git a/lib/Conch/Route/Workspace.pm b/lib/Conch/Route/Workspace.pm index 08adca85e..5294378bd 100644 --- a/lib/Conch/Route/Workspace.pm +++ b/lib/Conch/Route/Workspace.pm @@ -71,7 +71,7 @@ sub routes { $with_workspace_admin->get('/user')->to('workspace_user#get_all'); # POST /workspace/:workspace_id_or_name/user?send_mail=<1|0> - $with_workspace_admin->post('/user')->to('workspace_user#add_user'); + $with_workspace_admin->find_user_from_payload->post('/user')->to('workspace_user#add_user'); # DELETE /workspace/:workspace_id_or_name/user/#target_user_id_or_email?send_mail=<1|0> $with_workspace_admin->under('/user/#target_user_id_or_email')->to('user#find_user') ->delete('/')->to('workspace_user#remove'); @@ -83,12 +83,14 @@ __END__ =pod +=head1 ROUTE ENDPOINTS + All routes require authentication. Users will require access to the workspace (or one of its ancestors) at a minimum L, as indicated. -=head3 C +=head2 C =over 4 @@ -98,7 +100,7 @@ L, as indicated. =back -=head3 C +=head2 C =over 4 @@ -108,7 +110,7 @@ L, as indicated. =back -=head3 C +=head2 C =over 4 @@ -118,7 +120,7 @@ L, as indicated. =back -=head3 C<< POST /workspace/:workspace_id_or_name/child?send_mail=<1|0> >> +=head2 C<< POST /workspace/:workspace_id_or_name/child?send_mail=<1|0> >> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to C<1>) to send an email to the parent workspace admins. @@ -133,7 +135,7 @@ an email to the parent workspace admins. =back -=head3 C +=head2 C Accepts the following optional query parameters: @@ -157,7 +159,7 @@ Accepts the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -167,7 +169,7 @@ Accepts the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -177,7 +179,7 @@ Accepts the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -189,7 +191,7 @@ Accepts the following optional query parameters: =back -=head3 C +=head2 C =over 4 @@ -199,7 +201,7 @@ Accepts the following optional query parameters: =back -=head3 C +=head2 C Takes one query optional parameter, C to constrain results to those updated with in the last C minutes. @@ -212,7 +214,7 @@ those updated with in the last C minutes. =back -=head3 C +=head2 C =over 4 @@ -222,7 +224,7 @@ those updated with in the last C minutes. =back -=head3 C +=head2 C =over 4 @@ -232,7 +234,7 @@ those updated with in the last C minutes. =back -=head3 C<< POST /workspace/:workspace_id_or_name/user?send_mail=<1|0> >> +=head2 C<< POST /workspace/:workspace_id_or_name/user?send_mail=<1|0> >> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to C<1>) to send an email to the user and workspace admins. @@ -247,7 +249,7 @@ an email to the user and workspace admins. =back -=head3 C<< DELETE /workspace/:workspace_id_or_name/user/:target_user_id_or_email?send_mail=<1|0> >> +=head2 C<< DELETE /workspace/:workspace_id_or_name/user/:target_user_id_or_email?send_mail=<1|0> >> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to C<1>) to send an email to the user and workspace admins. @@ -260,7 +262,7 @@ an email to the user and workspace admins. =back -=head3 C +=head2 C =over 4 @@ -270,7 +272,7 @@ an email to the user and workspace admins. =back -=head3 C<< POST /workspace/:workspace_id_or_name/organization?send_mail=<1|0> >> +=head2 C<< POST /workspace/:workspace_id_or_name/organization?send_mail=<1|0> >> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the organization members and workspace admins. @@ -285,7 +287,7 @@ an email to the organization members and workspace admins. =back -=head3 C<< DELETE /workspace/:workspace_id_or_name/organization/:organization_id_or_name?send_mail=<1|0> >> +=head2 C<< DELETE /workspace/:workspace_id_or_name/organization/:organization_id_or_name?send_mail=<1|0> >> Takes one optional query parameter C<< send_mail=<1|0> >> (defaults to 1) to send an email to the organization members and workspace admins. diff --git a/lib/Test/Conch.pm b/lib/Test/Conch.pm index 7803347e9..13f5dc57b 100644 --- a/lib/Test/Conch.pm +++ b/lib/Test/Conch.pm @@ -502,8 +502,8 @@ sub generate_fixtures ($self, @specification) { Authenticates a user in the current test instance. Uses default (superuser) credentials if not provided. Optionally will bail out of B tests on failure. -This will set 'user' in the session (C<< $t->app->session('user') >>), so a token is not needed -on subsequent requests. +This will set 'user' in the session (C<< $t->ua->cookie_jar >>, accessed internally via +C<< $c->session('user_id') >>), so a token is not needed on subsequent requests. =cut @@ -511,9 +511,10 @@ sub authenticate ($self, %args) { $args{bailout} //= 1 if not $args{email}; $args{email} //= CONCH_EMAIL; $args{password} //= CONCH_PASSWORD; # note that if a fixture is used, everything is accepted (for speed) + $args{set_session} //= JSON::PP::true; local $Test::Builder::Level = $Test::Builder::Level + 1; - $self->post_ok('/login', json => { %args{qw(email password)} }) + $self->post_ok('/login', json => { %args{qw(email password set_session)} }) ->status_is(200, $args{message} // 'logged in as '.$args{email}) or $args{bailout} and Test::More::BAIL_OUT('Failed to log in as '.$args{email}); diff --git a/sql/migrations/0129-validation_result-result_order.sql b/sql/migrations/0129-validation_result-result_order.sql index 417c4f937..33269d725 100644 --- a/sql/migrations/0129-validation_result-result_order.sql +++ b/sql/migrations/0129-validation_result-result_order.sql @@ -1,5 +1,19 @@ SELECT run_migration(129, $$ + -- In order to speed up deployment time (this migration file takes many + -- tens of hours to run on a production database), we drop all historical + -- validation_results. The overall outcome of all the validations is still + -- captured in validation_state.status. If it is desired to later load + -- that historical data back into the database, start with a backup of + -- production-v2, delete the following two lines from this file, and run + -- all migrations against that database, then copy the validation_state_member + -- and validation_result tables into the master database: + -- pg_dump -U postgres -t validation_result -t validation_state_member source_database | psql -U conch conch + + truncate validation_state_member; + truncate validation_result; + + -- these are the two validation modules that can produce duplicate -- results, with the exception of the result_order. For cpu_temperature -- at least, we can infer the component value; for switch_peers we cannot diff --git a/sql/migrations/0144-validation_result-unique-constraint.sql b/sql/migrations/0144-validation_result-unique-constraint.sql new file mode 100644 index 000000000..c3438452d --- /dev/null +++ b/sql/migrations/0144-validation_result-unique-constraint.sql @@ -0,0 +1,16 @@ +SELECT run_migration(144, $$ + + -- We know that new validation results re-use old validation_result rows whenever possible + -- (see the end of Conch::ValidationSystem::run_validation_plan), and since release v3.0.0 + -- (where either: 1. merge_validation_results was run a final time, or 2. validation_result + -- was truncated) there are no duplicates, so it is now safe to turn this index into a + -- unique constraint. + + alter table validation_result + add constraint validation_result_all_columns_key unique + (device_id, hardware_product_id, validation_id, message, hint, status, category, component); + + drop index if exists validation_result_all_columns_idx; + drop index validation_result_device_id_idx; -- redundant with unique constraint + +$$); diff --git a/sql/schema.sql b/sql/schema.sql index 7c30d4c66..e04a6c728 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -1093,6 +1093,14 @@ ALTER TABLE ONLY public.validation_plan ADD CONSTRAINT validation_plan_pkey PRIMARY KEY (id); +-- +-- Name: validation_result validation_result_all_columns_key; Type: CONSTRAINT; Schema: public; Owner: conch +-- + +ALTER TABLE ONLY public.validation_result + ADD CONSTRAINT validation_result_all_columns_key UNIQUE (device_id, hardware_product_id, validation_id, message, hint, status, category, component); + + -- -- Name: validation_result validation_result_pkey; Type: CONSTRAINT; Schema: public; Owner: conch -- @@ -1442,20 +1450,6 @@ CREATE INDEX validation_plan_member_validation_plan_id_idx ON public.validation_ CREATE UNIQUE INDEX validation_plan_name_idx ON public.validation_plan USING btree (name) WHERE (deactivated IS NULL); --- --- Name: validation_result_all_columns_idx; Type: INDEX; Schema: public; Owner: conch --- - -CREATE INDEX validation_result_all_columns_idx ON public.validation_result USING btree (device_id, hardware_product_id, validation_id, message, hint, status, category, component); - - --- --- Name: validation_result_device_id_idx; Type: INDEX; Schema: public; Owner: conch --- - -CREATE INDEX validation_result_device_id_idx ON public.validation_result USING btree (device_id); - - -- -- Name: validation_result_hardware_product_id_idx; Type: INDEX; Schema: public; Owner: conch -- diff --git a/t/integration/crud/devices.t b/t/integration/crud/devices.t index 631678e51..61d145f64 100644 --- a/t/integration/crud/devices.t +++ b/t/integration/crud/devices.t @@ -634,7 +634,7 @@ subtest 'caching' => sub { $t->get_ok('/device/TEST') ->status_is(200) ->header_is('Cache-Control', 'no-cache') - ->header_exists('ETag') + ->header_like('ETag', qr{^W/"[^"]+"$}) ->json_schema_is('DetailedDevice') ->json_is($detailed_device); diff --git a/t/integration/users.t b/t/integration/users.t index d0fae726c..c9e1b5aac 100644 --- a/t/integration/users.t +++ b/t/integration/users.t @@ -170,6 +170,32 @@ subtest 'User' => sub { TEST3 => 'test3', }); + $t->post_ok('/user/me', json => { is_admin => JSON::PP::true }) + ->status_is(403) + ->email_not_sent; + + $t->post_ok('/user/me', json => $_) + ->status_is(409) + ->json_is({ error => 'duplicate user found' }) + ->email_not_sent + foreach + { email => 'conch@conch.joyent.us' }, + { email => 'cONcH@cONCh.joyent.us' }, + { name => 'conch' }; + + $t->post_ok('/user/me', json => { email => 'rO_USer@cONCh.joyent.us', name => 'rO_USer' }) + ->status_is(303) + ->location_is('/user/'.$ro_user->id) + ->email_cmp_deeply({ + To => '"rO_USer" ', + From => 'noreply@127.0.0.1', + Subject => 'Your Conch account has been updated', + body => re(qr/^Your account at \Q$JOYENT\E has been updated:\R\R {7}email: ro_user\@conch.joyent.us -> rO_USer\@cONCh.joyent.us\R {8}name: ro_user -> rO_USer\R\R/m), + }); + + $ro_user->discard_changes; + + # re-authenticate as the same user $t->authenticate(email => $ro_user->email); my @login_token = ($t->tx->res->json->{jwt_token}); { @@ -200,7 +226,9 @@ subtest 'User' => sub { { (map +($_ => $build1->$_), qw(id name description)), role => 'rw', role_via_organization_id => $organization->id }, { (map +($_ => $build2->$_), qw(id name description)), role => 'ro', role_via_organization_id => $organization->id }, ], - }); + }) + ->log_debug_is('attempting to authenticate with Authorization: Bearer header...'); + ; $user_detailed = $t2->tx->res->json; # the superuser always sees parent workspace ids $user_detailed->{workspaces}[0]{parent_workspace_id} = $child_ws->parent_workspace_id; @@ -240,11 +268,47 @@ subtest 'User' => sub { }, $super_user_data, $user_detailed), ]); - # get another JWT + is($ro_user->related_resultset('user_session_tokens')->count, 1, 'just 1 token presently'); + + # make the token look really old (not yet expired, but close to it) + $ro_user->related_resultset('user_session_tokens')->update({ created => '2000-01-01' }); + $ro_user->update({ password => '123' }); - $t->post_ok('/login', json => { email => $ro_user->email, password => '123' }) + $t->post_ok('/login', json => { email => $ro_user->email, password => '123', set_session => JSON::PP::false }) ->status_is(200); push @login_token, $t->tx->res->json->{jwt_token}; + + is($ro_user->related_resultset('user_session_tokens')->count, 2, 'a new token was created'); + + is($t->ua->cookie_jar->all->[0]->expires, 1, 'session cookie is expired'); + + $t->get_ok('/user/me') + ->status_is(401) + ->log_debug_is('auth failed: no credentials provided'); + + $t->post_ok('/login', json => { email => $ro_user->email, password => '123', set_session => JSON::PP::true }) + ->status_is(200) + ->json_schema_is('Login'); + + is($ro_user->related_resultset('user_session_tokens')->count, 2, 'got second login token again'); + + cmp_deeply( + $t->ua->cookie_jar->all->[0]->expires, + within_tolerance(time + 60*60*24, plus_or_minus => 10), + 'session expires approximately 1 day in the future', + ); + + $t->get_ok('/user/me') + ->status_is(200, 'can authenticate with the session again') + ->json_is('/id', $ro_user->id); + + $ro_user->discard_changes; + $ro_user->update({ refuse_session_auth => 1 }); + + $t->get_ok('/user/me') + ->status_is(401) + ->log_debug_is('user attempting to authenticate with session, but refuse_session_auth is set'); + { my $t2 = Test::Conch->new(pg => $t->pg); $t2->get_ok('/user/me', { Authorization => 'Bearer '.$login_token[1] }) @@ -252,6 +316,7 @@ subtest 'User' => sub { ->json_schema_is('UserDetailed') ->json_cmp_deeply({ $user_detailed->%*, + refuse_session_auth => bool(1), workspaces => [ map +{ $_->%*, parent_workspace_id => undef }, $user_detailed->{workspaces}->@* ], last_login => re(qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,9}Z$/), last_seen => re(qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,9}Z$/), @@ -265,18 +330,27 @@ subtest 'User' => sub { ->json_schema_is('UserDetailed') ->json_cmp_deeply({ $user_detailed->%*, + refuse_session_auth => bool(1), workspaces => [ map +{ $_->%*, parent_workspace_id => undef }, $user_detailed->{workspaces}->@* ], last_login => re(qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,9}Z$/), last_seen => re(qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,9}Z$/), }); } - $t->post_ok('/user/me/token', json => { name => 'an api token' }) + $ro_user->discard_changes; + $ro_user->update({ refuse_session_auth => 0 }); + + $t->post_ok('/login', json => { email => $ro_user->email, password => '123', set_session => JSON::PP::true }) + ->status_is(200); + + is($ro_user->related_resultset('user_session_tokens')->count, 2, 'got second login token again'); + + $t->post_ok('/user/me/token', { Authorization => 'Bearer '.$login_token[0] }, json => { name => 'an api token' }) ->status_is(201) ->location_is('/user/me/token/an api token'); my $api_token = $t->tx->res->json->{token}; - $t->post_ok('/user/me/password', json => { password => 'øƕḩẳȋ' }) + $t->post_ok('/user/me/password', { Authorization => 'Bearer '.$login_token[0] }, json => { password => 'øƕḩẳȋ' }) ->status_is(204, 'changed password') ->email_not_sent; @@ -291,12 +365,14 @@ subtest 'User' => sub { { my $t2 = Test::Conch->new(pg => $t->pg); $t2->get_ok('/user/me', { Authorization => 'Bearer '.$login_token[0] }) - ->status_is(401, 'main login token no longer works after changing password'); + ->status_is(401, 'main login token no longer works after changing password') + ->log_debug_is('auth failed: JWT for user_id '.$ro_user->id.' could not be found'); } { my $t2 = Test::Conch->new(pg => $t->pg); $t2->get_ok('/user/me', { Authorization => 'Bearer '.$login_token[1] }) - ->status_is(401, 'second login token no longer works after changing password'); + ->status_is(401, 'second login token no longer works after changing password') + ->log_debug_is('auth failed: JWT for user_id '.$ro_user->id.' could not be found'); } { @@ -314,27 +390,30 @@ subtest 'User' => sub { $t->post_ok('/login', json => { email => $ro_user->email, password => 'øƕḩẳȋ' }) ->status_is(200) - ->log_info_is('user ro_user ('.$ro_user->email.') logged in'); + ->log_info_is('user rO_USer ('.$ro_user->email.') logged in'); - $t->post_ok('/user/me/password?clear_tokens=all', json => { password => 'another password' }) + $t->post_ok('/user/me/password?clear_tokens=all', { Authorization => 'Bearer '.$t->tx->res->json->{jwt_token} }, + json => { password => 'another password' }) ->status_is(204, 'changed password again'); { my $t2 = Test::Conch->new(pg => $t->pg); $t2->get_ok('/user/me', { Authorization => 'Bearer '.$api_token }) - ->status_is(401, 'api login token no longer works either'); + ->status_is(401, 'api login token no longer works either') + ->log_debug_is('auth failed: JWT for user_id '.$ro_user->id.' could not be found'); } $t->post_ok('/login', json => { user_id => $user_detailed->{id}, password => 'another password' }) ->status_is(200, 'logged in using second new password, and user_id instead of email'); - $t->post_ok('/user/me/password', json => { password => '123' }) + $t->post_ok('/user/me/password', { Authorization => 'Bearer '.$t->tx->res->json->{jwt_token} }, + json => { password => '123' }) ->status_is(204, 'changed password back to original'); $t->post_ok('/login', json => { email => $ro_user->email, password => '123' }) ->status_is(200, 'logged in using original password'); - $t->get_ok('/user/me/settings') + $t->get_ok('/user/me/settings', { Authorization => 'Bearer '.$t->tx->res->json->{jwt_token} }) ->status_is(200, 'original password works again'); # reset db password entry to '' so we don't have to remember our password string @@ -350,11 +429,12 @@ subtest 'Log out' => sub { $t->post_ok('/logout') ->status_is(204); $t->get_ok('/workspace') - ->status_is(401); + ->status_is(401) + ->log_debug_is('auth failed: no credentials provided'); }; subtest 'JWT authentication' => sub { - $t->authenticate(email => $ro_user->email, bailout => 0) + $t->post_ok('/login', json => { email => $ro_user->email, password => '..' }) ->status_is(200) ->header_exists('Last-Modified') ->header_exists('Expires') @@ -363,11 +443,11 @@ subtest 'JWT authentication' => sub { my $jwt_token = $t->tx->res->json->{jwt_token}; - $t->reset_session; # force JWT to be used to authenticate + is($t->ua->cookie_jar->all->[0]->expires, 1, 'session cookie is expired'); + $t->get_ok('/workspace', { Authorization => 'Bearer '.$jwt_token }) ->status_is(200, 'user can provide Authentication header with full JWT to authenticate'); - $t->reset_session; # we're going to be cheeky here and hack the JWT to doctor it... # this only works because we have access to the symmetric secret embedded in the app. my $jwt_claims = Mojo::JWT->new(secret => $t->app->secrets->[0])->decode($jwt_token); @@ -378,8 +458,8 @@ subtest 'JWT authentication' => sub { expires => $jwt_claims->{exp}, )->encode; $t->get_ok('/workspace', { Authorization => 'Bearer '.$hacked_jwt_token }) - ->status_is(401, 'the user_id is verified in the JWT'); - $t->log_debug_is('auth failed: JWT for user_id '.$bad_user_id.' could not be found'); + ->status_is(401) + ->log_debug_is('auth failed: JWT for user_id '.$bad_user_id.' could not be found'); $t->post_ok('/refresh_token', { Authorization => 'Bearer '.$jwt_token }) ->status_is(200) @@ -388,11 +468,9 @@ subtest 'JWT authentication' => sub { my $new_jwt_token = $t->tx->res->json->{jwt_token}; $t->get_ok('/workspace', { Authorization => 'Bearer '.$new_jwt_token }) ->status_is(200, 'Can authenticate with new token'); - $t->get_ok('/workspace', { Authorization => 'Bearer '.$jwt_token }) - ->status_is(401, 'Cannot use old token'); - $t->get_ok('/me', { Authorization => 'Bearer '.$jwt_token }) - ->status_is(401, 'Cannot reuse old JWT'); + ->status_is(401, 'Cannot use old token') + ->log_debug_is('auth failed: JWT for user_id '.$ro_user->id.' could not be found'); $t_super->get_ok('/me', { Authorization => 'Bearer '.$new_jwt_token }) ->status_is(401, 'cannot use other user\'s JWT') @@ -423,11 +501,16 @@ subtest 'JWT authentication' => sub { ]); $t->get_ok('/workspace', { Authorization => "Bearer $new_jwt_token" }) - ->status_is(401, 'Cannot use after user revocation'); + ->status_is(401, 'Cannot use token or session after user revocation') + ->log_debug_is('auth failed: JWT for user_id '.$ro_user->id.' could not be found'); + $t->post_ok('/refresh_token', { Authorization => "Bearer $new_jwt_token" }) ->status_is(401, 'Cannot use after user revocation'); - $t->authenticate(email => $ro_user->email, bailout => 0); + $t->post_ok('/login', json => { email => $ro_user->email, password => '..' }) + ->status_is(200) + ->json_schema_is('Login'); + my $jwt_token_2 = $t->tx->res->json->{jwt_token}; $t->post_ok('/user/me/revoke', { Authorization => "Bearer $jwt_token_2" }) ->status_is(204, 'Revoke tokens for self') @@ -614,11 +697,12 @@ subtest 'modify another user' => sub { ->json_is($new_user_data); my $t2 = Test::Conch->new(pg => $t->pg); - $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => '123' }) + $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => '123', set_session => JSON::PP::true }) ->status_is(200, 'new user can log in'); my $jwt_token = $t2->tx->res->json->{jwt_token}; - $t2->get_ok('/me')->status_is(204); + $t2->get_ok('/me', { Authorization => 'Bearer '.$jwt_token }) + ->status_is(204); $t2->post_ok('/user/me/token', json => { name => 'my api token' }) ->header_exists('Last-Modified') @@ -664,7 +748,7 @@ subtest 'modify another user' => sub { $t2->get_ok('/me', { Authorization => "Bearer $api_token" }) ->status_is(401, 'new user cannot authenticate with the api token after api tokens are revoked'); - $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => '123' }) + $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => '123', set_session => JSON::PP::true }) ->status_is(200, 'new user can still log in again'); $jwt_token = $t2->tx->res->json->{jwt_token}; @@ -743,7 +827,7 @@ subtest 'modify another user' => sub { ->status_is(401) ->log_debug_is('password validation for untrusted@conch.joyent.us failed'); - $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => $insecure_password }) + $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => $insecure_password, set_session => JSON::PP::true }) ->status_is(200) ->log_info_is('user UNTRUSTED (untrusted@conch.joyent.us) logging in with one-time insecure password') ->location_is('/user/me/password'); @@ -772,7 +856,7 @@ subtest 'modify another user' => sub { my $secure_password = $_new_password; is($secure_password, 'a more secure password', 'provided password was saved to the db'); - $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => $secure_password }) + $t2->post_ok('/login', json => { email => 'untrusted@conch.joyent.us', password => $secure_password, set_session => JSON::PP::true }) ->status_is(200) ->log_info_is('user UNTRUSTED (untrusted@conch.joyent.us) logged in') ->json_has('/jwt_token') @@ -781,7 +865,7 @@ subtest 'modify another user' => sub { $t2->get_ok('/me') ->status_is(204) - ->log_debug_is('using session user='.$new_user_id) + ->log_debug_is('using session user_id='.$new_user_id) ->log_debug_is('looking up user by id '.$new_user_id.': found UNTRUSTED (untrusted@conch.joyent.us)'); is($t2->tx->res->body, '', '...with no extra response messages'); diff --git a/t/schema.t b/t/schema.t index 11e73210c..87500bf58 100644 --- a/t/schema.t +++ b/t/schema.t @@ -340,6 +340,7 @@ $t->get_ok('/schema/request/Login') user_id => { '$ref' => '/definitions/uuid' }, email => { '$ref' => '/definitions/email_address' }, password => { '$ref' => '/definitions/non_empty_string' }, + set_session => { type => 'boolean', default => JSON::PP::false }, }, definitions => { non_empty_string => { type => 'string', minLength => 1 },