Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 229 additions & 1 deletion OPENAPI_DOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12823,6 +12823,113 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ContentError'
/api/engine/v2/modules/{id}/state/{key}/value:
summary: Modules, running instances of drivers that control individual devices
and services
get:
summary: Returns the value of the requested status variable as JSON, `null`
when it isn't set.
description: 'Returns the value of the requested status variable as JSON, `null`
when it isn''t set.

Subscribe to the resource to be notified when the value changes'
tags:
- Modules
operationId: PlaceOS::Api::Modules_state_value
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: key
in: path
description: the name of the status variable
example: power
required: true
schema:
type: string
responses:
"200":
description: OK
"409":
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"503":
description: Service Unavailable
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ImageGenError'
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ImageGenError'
"404":
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"429":
description: Too Many Requests
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ImageGenError'
"422":
description: Unprocessable Entity
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ParameterError'
"502":
description: Bad Gateway
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"504":
description: Gateway Timeout
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"406":
description: Not Acceptable
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ContentError'
"408":
description: Request Timeout
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ParameterError'
"415":
description: Unsupported Media Type
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ContentError'
/api/engine/v2/modules/{id}/state/{key}:
summary: Modules, running instances of drivers that control individual devices
and services
Expand Down Expand Up @@ -12928,6 +13035,124 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ContentError'
/api/engine/v2/modules/{id}/logs:
summary: Modules, running instances of drivers that control individual devices
and services
get:
summary: Captures the module's debug output for a period of time, as lines of
description: 'Captures the module''s debug output for a period of time, as lines
of

`<time> <LEVEL> <message>`. Over HTTP the lines are streamed as they arrive.

MCP clients receive them once the capture ends, so capturing for more than
a

minute may not work with all clients. The output is limited to 3 MiB.'
tags:
- Modules
operationId: PlaceOS::Api::Modules_logs
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: seconds
in: query
description: how long to capture logs for, in seconds, up to 129600 (36 hours)
example: 30
schema:
type: integer
format: Int32
- name: level
in: query
description: 'the minimum level of the lines captured: trace, debug, info,
notice, warn, error or fatal'
example: info
schema:
type: string
responses:
"200":
description: OK
"409":
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"503":
description: Service Unavailable
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ImageGenError'
"403":
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ImageGenError'
"404":
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"429":
description: Too Many Requests
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ImageGenError'
"422":
description: Unprocessable Entity
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ParameterError'
"502":
description: Bad Gateway
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"504":
description: Gateway Timeout
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"406":
description: Not Acceptable
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ContentError'
"408":
description: Request Timeout
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.CommonError'
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ParameterError'
"415":
description: Unsupported Media Type
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOS.Api.Application.ContentError'
/api/engine/v2/modules/{id}/ping:
summary: Modules, running instances of drivers that control individual devices
and services
Expand Down Expand Up @@ -15034,7 +15259,10 @@ paths:
summary: Systems are typically rooms or spaces, they can represent digital signage
endpoints and are a collection of modules (devices) placed in zones
get:
summary: Returns the state lookup for a given key on a module
summary: Returns the state lookup for a given key on a module.
description: 'Returns the state lookup for a given key on a module.

Subscribe to the resource to be notified when the value changes'
tags:
- Systems
operationId: PlaceOS::Api::Systems_state_lookup
Expand Down
2 changes: 1 addition & 1 deletion shard.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ shards:

action-controller:
git: https://github.com/spider-gazelle/action-controller.git
version: 9.2.0
version: 9.3.0

active-model:
git: https://github.com/spider-gazelle/active-model.git
Expand Down
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies:
# Server framework
action-controller:
github: spider-gazelle/action-controller
version: ">= 8.3.1"
version: ">= 9.3.0"

# Data validation library
active-model:
Expand Down
89 changes: 89 additions & 0 deletions spec/controllers/modules_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,95 @@ module PlaceOS::Api
zone.destroy
end

it "gates GET state value on the Read bit, returning the stored JSON as is" do
authority = Model::Authority.find_by_domain("localhost").not_nil!
user, headers = Spec::Authentication.authentication(sys_admin: false, support: false)

zone = Model::Generator.zone.save!
mod = support_setup.call(zone)
state_path = File.join(Modules.base_route, mod.id.as(String), "state")
PlaceOS::Driver::RedisStorage.new(mod.id.as(String))["power"] = %({"on":true,"level":2})

group = Model::Generator.group(authority: authority, subsystems: ["support"]).save!
gu = Model::Generator.group_user(user: user, group: group, permissions: Model::Permissions::Update).save!
gz = Model::Generator.group_zone(group: group, zone: zone, permissions: Model::Permissions::Update).save!

client.get(path: File.join(state_path, "power", "value"), headers: headers).status_code.should eq 403

gu.permissions = Model::Permissions::Read.to_i
gu.save!
gz.permissions = Model::Permissions::Read.to_i
gz.save!

result = client.get(path: File.join(state_path, "power", "value"), headers: headers)
result.status_code.should eq 200
result.headers["Content-Type"].should start_with "application/json"
JSON.parse(result.body).should eq JSON.parse(%({"on":true,"level":2}))

# the existing lookup still returns the value encoded as a string
String.from_json(client.get(path: File.join(state_path, "power"), headers: headers).body).should eq %({"on":true,"level":2})

missing = client.get(path: File.join(state_path, "missing", "value"), headers: headers)
missing.status_code.should eq 200
missing.body.should eq "null"

mod.destroy
zone.destroy
end

it "gates GET logs on the Operate and Read bits" do
authority = Model::Authority.find_by_domain("localhost").not_nil!
user, headers = Spec::Authentication.authentication(sys_admin: false, support: false)

zone = Model::Generator.zone.save!
mod = support_setup.call(zone)
logs_path = File.join(Modules.base_route, mod.id.as(String), "logs?seconds=1")

group = Model::Generator.group(authority: authority, subsystems: ["support"]).save!
gu = Model::Generator.group_user(user: user, group: group, permissions: Model::Permissions::Read).save!
gz = Model::Generator.group_zone(group: group, zone: zone, permissions: Model::Permissions::Read).save!

# Read alone is not enough
client.get(path: logs_path, headers: headers).status_code.should eq 403

# nor is Operate alone
gu.permissions = Model::Permissions::Operate.to_i
gu.save!
gz.permissions = Model::Permissions::Operate.to_i
gz.save!
client.get(path: logs_path, headers: headers).status_code.should eq 403

# Operate and Read => past the auth gate. Only the auth outcome is asserted,
# as core may not be running the module
both = (Model::Permissions::Operate | Model::Permissions::Read).to_i
gu.permissions = both
gu.save!
gz.permissions = both
gz.save!
client.get(path: logs_path, headers: headers).status_code.should_not eq 403

# support users can always capture logs
client.get(path: logs_path, headers: Spec::Authentication.headers(sys_admin: false, support: true)).status_code.should_not eq 403

mod.destroy
zone.destroy
end

it "validates the GET logs params" do
zone = Model::Generator.zone.save!
mod = support_setup.call(zone)
logs_path = File.join(Modules.base_route, mod.id.as(String), "logs")
headers = Spec::Authentication.headers

client.get(path: "#{logs_path}?seconds=0", headers: headers).status_code.should eq 400
client.get(path: "#{logs_path}?seconds=#{Modules::LOGS_MAX_SECONDS + 1}", headers: headers).status_code.should eq 400
client.get(path: "#{logs_path}?seconds=1&level=loud", headers: headers).status_code.should eq 400
client.get(path: "#{logs_path}?seconds=1&level=none", headers: headers).status_code.should eq 400

mod.destroy
zone.destroy
end

it "keeps GET error support-JWT only, even for Manage grants" do
authority = Model::Authority.find_by_domain("localhost").not_nil!
user, headers = Spec::Authentication.authentication(sys_admin: false, support: false)
Expand Down
Loading
Loading