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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ fm pages publish --owner growth --html-file report.html --visibility unlisted
fm pages asset upload chart.png
```

FeedMob Workspace is the internal, read-only API for shared operational data.
Obtain a personal access token from FeedMob SSO, then authenticate without
placing the token in shell history. Workspace requests are GET-only and must
stay under `/api/v1/`; access to individual resources is enforced by the
server-side Workspace privileges. Consult the SSO-protected API Reference for
available resources rather than treating CLI help as an endpoint catalog.
FeedMob Workspace is the CLI's read-only access to the FeedMob Admin API for
shared operational data. Obtain a personal access token from FeedMob SSO, then
authenticate without placing the token in shell history. The token owner must
have an approved SSO account. A `401 Unauthorized` response can mean that the
token is invalid, expired, or revoked, or that the account is not approved.
Workspace requests are GET-only and must stay under `/api/v1/`. Consult the
SSO-protected API Reference for available resources rather than treating CLI
help as an endpoint catalog.

```sh
fm workspace auth login
Expand Down
14 changes: 14 additions & 0 deletions lib/feedmob/cli/http/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def raise_api_error!(status, data)
end

def api_error(data, status)
return workspace_unauthorized_error if workspace_unauthorized?(status)

remote_error = data['error'] if data.is_a?(Hash)
if remote_error.is_a?(Hash)
[remote_error['code'] || 'http_error', remote_error['message'] || default_api_error(status)]
Expand All @@ -102,6 +104,18 @@ def data_message(data, remote_error, status)
def default_api_error(status)
"#{@service.label} API returned HTTP #{status}."
end

def workspace_unauthorized?(status)
@service.name == 'workspace' && status == 401
end

def workspace_unauthorized_error
[
'http_error',
'FeedMob Workspace could not authenticate with the FeedMob Admin API. ' \
'Confirm that the personal access token is active and the FeedMob SSO account is approved.'
]
end
end
end
end
Expand Down
23 changes: 23 additions & 0 deletions test/http_client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,27 @@ def test_api_error_uses_remote_message_without_exposing_token
assert_equal 'Credential expired.', error.message
refute_includes error.message, 'fmpat_never-print-me'
end

def test_workspace_unauthorized_error_explains_the_account_approval_requirement
workspace_service = FeedMob::CLI::Services.fetch('workspace', env: {})
transport = FakeTransport.new(
response: {
status: 401,
headers: { 'content-type' => 'application/json' },
body: '{"error":"Unauthorized"}'
}
)
client = FeedMob::CLI::HTTP::Client.new(service: workspace_service, transport:)

error = assert_raises(FeedMob::CLI::Error) do
client.request(method: :get, path: '/api/v1/me', token: 'fmapat_never-print-me')
end

expected_message = 'FeedMob Workspace could not authenticate with the FeedMob Admin API. ' \
'Confirm that the personal access token is active and the FeedMob SSO account is approved.'
assert_equal 'http_error', error.code
assert_equal expected_message, error.message
assert_equal({ status: 401 }, error.details)
refute_includes error.message, 'fmapat_never-print-me'
end
end
Loading