Merge pull request 'support for multiple admin API token' (#982) from multi-admin-token into next-v2
Reviewed-on: #982
This commit is contained in:
commit
fd2472d488
25 changed files with 1454 additions and 141 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1298,6 +1298,7 @@ dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytesize",
|
"bytesize",
|
||||||
|
"chrono",
|
||||||
"err-derive",
|
"err-derive",
|
||||||
"format_table",
|
"format_table",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -1467,6 +1468,7 @@ dependencies = [
|
||||||
name = "garage_model"
|
name = "garage_model"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"blake2",
|
"blake2",
|
||||||
|
|
|
@ -48,7 +48,7 @@ blake2 = "0.10"
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
bytesize = "1.1"
|
bytesize = "1.1"
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
chrono = "0.4"
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
crc32fast = "1.4"
|
crc32fast = "1.4"
|
||||||
crc32c = "0.6"
|
crc32c = "0.6"
|
||||||
crypto-common = "0.1"
|
crypto-common = "0.1"
|
||||||
|
@ -101,7 +101,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "rc"]
|
||||||
serde_bytes = "0.11"
|
serde_bytes = "0.11"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = { version = "0.8", default-features = false, features = ["parse"] }
|
toml = { version = "0.8", default-features = false, features = ["parse"] }
|
||||||
utoipa = "5.3.1"
|
utoipa = { version = "5.3.1", features = ["chrono"] }
|
||||||
|
|
||||||
# newer version requires rust edition 2021
|
# newer version requires rust edition 2021
|
||||||
k8s-openapi = { version = "0.21", features = ["v1_24"] }
|
k8s-openapi = { version = "0.21", features = ["v1_24"] }
|
||||||
|
|
|
@ -225,6 +225,40 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/CreateAdminToken": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Admin API token"
|
||||||
|
],
|
||||||
|
"description": "Creates a new admin API token",
|
||||||
|
"operationId": "CreateAdminToken",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateAdminTokenRequestBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Admin token has been created",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateAdminTokenResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/CreateBucket": {
|
"/v2/CreateBucket": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -325,6 +359,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/DeleteAdminToken": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Admin API token"
|
||||||
|
],
|
||||||
|
"description": "Delete an admin API token from the cluster, revoking all its permissions.",
|
||||||
|
"operationId": "DeleteAdminToken",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Admin API token ID",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Admin token has been deleted"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/DeleteBucket": {
|
"/v2/DeleteBucket": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -415,6 +474,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/GetAdminTokenInfo": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Admin API token"
|
||||||
|
],
|
||||||
|
"description": "\nReturn information about a specific admin API token.\nYou can search by specifying the exact token identifier (`id`) or by specifying a pattern (`search`).\n ",
|
||||||
|
"operationId": "GetAdminTokenInfo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Admin API token ID",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Partial token ID or name to search for",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Information about the admin token",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GetAdminTokenInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/GetBlockInfo": {
|
"/v2/GetBlockInfo": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -886,6 +983,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/ListAdminTokens": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Admin API token"
|
||||||
|
],
|
||||||
|
"description": "Returns all admin API tokens in the cluster.",
|
||||||
|
"operationId": "ListAdminTokens",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns info about all admin API tokens",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ListAdminTokensResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/ListBlockErrors": {
|
"/v2/ListBlockErrors": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -1216,6 +1337,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/UpdateAdminToken": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Admin API token"
|
||||||
|
],
|
||||||
|
"description": "\nUpdates information about the specified admin API token.\n ",
|
||||||
|
"operationId": "UpdateAdminToken",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Admin API token ID",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateAdminTokenRequestBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Admin token has been updated",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateAdminTokenResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/UpdateBucket": {
|
"/v2/UpdateBucket": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -1775,6 +1938,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreateAdminTokenResponse": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/GetAdminTokenInfoResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"secretToken"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"secretToken": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The secret bearer token. **CAUTION:** This token will be shown only\nONCE, so this value MUST be remembered somewhere, or the token\nwill be unusable."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"CreateBucketLocalAlias": {
|
"CreateBucketLocalAlias": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -1858,6 +2040,54 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GetAdminTokenInfoResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"expired",
|
||||||
|
"scope"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"created": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Creation date"
|
||||||
|
},
|
||||||
|
"expiration": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Expiration time and date, formatted according to RFC 3339"
|
||||||
|
},
|
||||||
|
"expired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether this admin token is expired already"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Identifier of the admin token (which is also a prefix of the full bearer token)"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the admin API token"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"GetBucketInfoKey": {
|
"GetBucketInfoKey": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -2325,6 +2555,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ListAdminTokensResponse": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/GetAdminTokenInfoResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ListBucketsResponse": {
|
"ListBucketsResponse": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -3404,6 +3640,39 @@
|
||||||
"cancel"
|
"cancel"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"UpdateAdminTokenRequestBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiration": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "Expiration time and date, formatted according to RFC 3339"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Name of the admin API token"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints. **WARNING:** Granting a scope of `CreateAdminToken` or\n`UpdateAdminToken` trivially allows for privilege escalation, and is thus\nfunctionnally equivalent to granting a scope of `*`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateAdminTokenResponse": {
|
||||||
|
"$ref": "#/components/schemas/GetAdminTokenInfoResponse"
|
||||||
|
},
|
||||||
"UpdateBucketRequestBody": {
|
"UpdateBucketRequestBody": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -80,6 +80,7 @@ add_host_to_metrics = true
|
||||||
[admin]
|
[admin]
|
||||||
api_bind_addr = "0.0.0.0:3903"
|
api_bind_addr = "0.0.0.0:3903"
|
||||||
metrics_token = "BCAdFjoa9G0KJR0WXnHHm7fs1ZAbfpI8iIZ+Z/a2NgI="
|
metrics_token = "BCAdFjoa9G0KJR0WXnHHm7fs1ZAbfpI8iIZ+Z/a2NgI="
|
||||||
|
metrics_require_token = true
|
||||||
admin_token = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4="
|
admin_token = "UkLeGWEvHnXBqnueR3ISEMWpOnm40jH2tM2HnnL/0F4="
|
||||||
trace_sink = "http://localhost:4317"
|
trace_sink = "http://localhost:4317"
|
||||||
```
|
```
|
||||||
|
@ -145,6 +146,7 @@ The `[s3_web]` section:
|
||||||
|
|
||||||
The `[admin]` section:
|
The `[admin]` section:
|
||||||
[`api_bind_addr`](#admin_api_bind_addr),
|
[`api_bind_addr`](#admin_api_bind_addr),
|
||||||
|
[`metrics_require_token`](#admin_metrics_require_token),
|
||||||
[`metrics_token`/`metrics_token_file`](#admin_metrics_token),
|
[`metrics_token`/`metrics_token_file`](#admin_metrics_token),
|
||||||
[`admin_token`/`admin_token_file`](#admin_token),
|
[`admin_token`/`admin_token_file`](#admin_token),
|
||||||
[`trace_sink`](#admin_trace_sink),
|
[`trace_sink`](#admin_trace_sink),
|
||||||
|
@ -767,10 +769,34 @@ See [administration API reference](@/documentation/reference-manual/admin-api.md
|
||||||
Alternatively, since `v0.8.5`, a path can be used to create a unix socket. Note that for security reasons,
|
Alternatively, since `v0.8.5`, a path can be used to create a unix socket. Note that for security reasons,
|
||||||
the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
|
the socket will have 0220 mode. Make sure to set user and group permissions accordingly.
|
||||||
|
|
||||||
|
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
||||||
|
|
||||||
|
The token for accessing all administration functions on the admin endpoint,
|
||||||
|
with the exception of the metrics endpoint (see `metrics_token`).
|
||||||
|
|
||||||
|
You can use any random string for this value. We recommend generating a random
|
||||||
|
token with `openssl rand -base64 32`.
|
||||||
|
|
||||||
|
For Garage version earlier than `v2.0`, if this token is not set,
|
||||||
|
access to these endpoints is disabled entirely.
|
||||||
|
|
||||||
|
Since Garage `v2.0`, additional admin API tokens can be defined dynamically
|
||||||
|
in your Garage cluster using administration commands. This new admin token system
|
||||||
|
is more flexible since it allows admin tokens to have an expiration date,
|
||||||
|
and to have a scope restricted to certain admin API functions. If `admin_token`
|
||||||
|
is set, it behaves as an admin token without expiration and with full scope.
|
||||||
|
Otherwise, only admin API tokens defined dynamically can be used.
|
||||||
|
|
||||||
|
`admin_token` was introduced in Garage `v0.7.2`.
|
||||||
|
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
||||||
|
|
||||||
|
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||||
|
|
||||||
#### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN`, `GARAGE_METRICS_TOKEN_FILE` (env) {#admin_metrics_token}
|
#### `metrics_token`, `metrics_token_file` or `GARAGE_METRICS_TOKEN`, `GARAGE_METRICS_TOKEN_FILE` (env) {#admin_metrics_token}
|
||||||
|
|
||||||
The token for accessing the Metrics endpoint. If this token is not set, the
|
The token for accessing the Prometheus metrics endpoint (`/metrics`).
|
||||||
Metrics endpoint can be accessed without access control.
|
If this token is not set, and unless `metrics_require_token` is set to `true`,
|
||||||
|
the metrics endpoint can be accessed without access control.
|
||||||
|
|
||||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
||||||
|
|
||||||
|
@ -779,17 +805,12 @@ You can use any random string for this value. We recommend generating a random t
|
||||||
|
|
||||||
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
`GARAGE_METRICS_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
||||||
|
|
||||||
#### `admin_token`, `admin_token_file` or `GARAGE_ADMIN_TOKEN`, `GARAGE_ADMIN_TOKEN_FILE` (env) {#admin_token}
|
#### `metrics_require_token` (since `v2.0.0`) {#admin_metrics_require_token}
|
||||||
|
|
||||||
The token for accessing all of the other administration endpoints. If this
|
If this is set to `true`, accessing the metrics endpoint will always require
|
||||||
token is not set, access to these endpoints is disabled entirely.
|
an access token. Valid tokens include the `metrics_token` if it is set,
|
||||||
|
and admin API token defined dynamicaly in Garage which have
|
||||||
You can use any random string for this value. We recommend generating a random token with `openssl rand -base64 32`.
|
the `Metrics` endpoint in their scope.
|
||||||
|
|
||||||
`admin_token` was introduced in Garage `v0.7.2`.
|
|
||||||
`admin_token_file` and the `GARAGE_ADMIN_TOKEN` environment variable are supported since Garage `v0.8.2`.
|
|
||||||
|
|
||||||
`GARAGE_ADMIN_TOKEN_FILE` is supported since `v0.8.5` / `v0.9.1`.
|
|
||||||
|
|
||||||
#### `trace_sink` {#admin_trace_sink}
|
#### `trace_sink` {#admin_trace_sink}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ garage_api_common.workspace = true
|
||||||
argon2.workspace = true
|
argon2.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
err-derive.workspace = true
|
err-derive.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
paste.workspace = true
|
paste.workspace = true
|
||||||
|
|
225
src/api/admin/admin_token.rs
Normal file
225
src/api/admin/admin_token.rs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use garage_table::*;
|
||||||
|
use garage_util::time::now_msec;
|
||||||
|
|
||||||
|
use garage_model::admin_token_table::*;
|
||||||
|
use garage_model::garage::Garage;
|
||||||
|
|
||||||
|
use crate::api::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::{Admin, RequestHandler};
|
||||||
|
|
||||||
|
impl RequestHandler for ListAdminTokensRequest {
|
||||||
|
type Response = ListAdminTokensResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<ListAdminTokensResponse, Error> {
|
||||||
|
let now = now_msec();
|
||||||
|
|
||||||
|
let mut res = garage
|
||||||
|
.admin_token_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
|
||||||
|
10000,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|t| admin_token_info_results(t, now))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if garage.config.admin.admin_token.is_some() {
|
||||||
|
res.insert(
|
||||||
|
0,
|
||||||
|
GetAdminTokenInfoResponse {
|
||||||
|
id: None,
|
||||||
|
created: None,
|
||||||
|
name: "admin_token (from daemon configuration)".into(),
|
||||||
|
expiration: None,
|
||||||
|
expired: false,
|
||||||
|
scope: vec!["*".into()],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if garage.config.admin.metrics_token.is_some() {
|
||||||
|
res.insert(
|
||||||
|
1,
|
||||||
|
GetAdminTokenInfoResponse {
|
||||||
|
id: None,
|
||||||
|
created: None,
|
||||||
|
name: "metrics_token (from daemon configuration)".into(),
|
||||||
|
expiration: None,
|
||||||
|
expired: false,
|
||||||
|
scope: vec!["Metrics".into()],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ListAdminTokensResponse(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for GetAdminTokenInfoRequest {
|
||||||
|
type Response = GetAdminTokenInfoResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<GetAdminTokenInfoResponse, Error> {
|
||||||
|
let token = match (self.id, self.search) {
|
||||||
|
(Some(id), None) => get_existing_admin_token(garage, &id).await?,
|
||||||
|
(None, Some(search)) => {
|
||||||
|
let candidates = garage
|
||||||
|
.admin_token_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
|
||||||
|
10,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if candidates.len() != 1 {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"{} matching admin tokens",
|
||||||
|
candidates.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
candidates.into_iter().next().unwrap()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::bad_request(
|
||||||
|
"Either id or search must be provided (but not both)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(admin_token_info_results(&token, now_msec()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for CreateAdminTokenRequest {
|
||||||
|
type Response = CreateAdminTokenResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<CreateAdminTokenResponse, Error> {
|
||||||
|
let (mut token, secret) = if self.0.name.is_some() {
|
||||||
|
AdminApiToken::new("")
|
||||||
|
} else {
|
||||||
|
AdminApiToken::new(&format!("token_{}", Utc::now().format("%Y%m%d_%H%M")))
|
||||||
|
};
|
||||||
|
|
||||||
|
apply_token_updates(&mut token, self.0);
|
||||||
|
|
||||||
|
garage.admin_token_table.insert(&token).await?;
|
||||||
|
|
||||||
|
Ok(CreateAdminTokenResponse {
|
||||||
|
secret_token: secret,
|
||||||
|
info: admin_token_info_results(&token, now_msec()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for UpdateAdminTokenRequest {
|
||||||
|
type Response = UpdateAdminTokenResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<UpdateAdminTokenResponse, Error> {
|
||||||
|
let mut token = get_existing_admin_token(&garage, &self.id).await?;
|
||||||
|
|
||||||
|
apply_token_updates(&mut token, self.body);
|
||||||
|
|
||||||
|
garage.admin_token_table.insert(&token).await?;
|
||||||
|
|
||||||
|
Ok(UpdateAdminTokenResponse(admin_token_info_results(
|
||||||
|
&token,
|
||||||
|
now_msec(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHandler for DeleteAdminTokenRequest {
|
||||||
|
type Response = DeleteAdminTokenResponse;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
self,
|
||||||
|
garage: &Arc<Garage>,
|
||||||
|
_admin: &Admin,
|
||||||
|
) -> Result<DeleteAdminTokenResponse, Error> {
|
||||||
|
let token = get_existing_admin_token(&garage, &self.id).await?;
|
||||||
|
|
||||||
|
garage
|
||||||
|
.admin_token_table
|
||||||
|
.insert(&AdminApiToken::delete(token.prefix))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(DeleteAdminTokenResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInfoResponse {
|
||||||
|
let params = token.params().unwrap();
|
||||||
|
|
||||||
|
GetAdminTokenInfoResponse {
|
||||||
|
id: Some(token.prefix.clone()),
|
||||||
|
created: Some(
|
||||||
|
DateTime::from_timestamp_millis(params.created as i64)
|
||||||
|
.expect("invalid timestamp stored in db"),
|
||||||
|
),
|
||||||
|
name: params.name.get().to_string(),
|
||||||
|
expiration: params.expiration.get().map(|x| {
|
||||||
|
DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db")
|
||||||
|
}),
|
||||||
|
expired: params
|
||||||
|
.expiration
|
||||||
|
.get()
|
||||||
|
.map(|exp| now > exp)
|
||||||
|
.unwrap_or(false),
|
||||||
|
scope: params.scope.get().0.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_existing_admin_token(garage: &Garage, id: &String) -> Result<AdminApiToken, Error> {
|
||||||
|
garage
|
||||||
|
.admin_token_table
|
||||||
|
.get(&EmptyKey, id)
|
||||||
|
.await?
|
||||||
|
.filter(|k| !k.state.is_deleted())
|
||||||
|
.ok_or_else(|| Error::NoSuchAdminToken(id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_token_updates(token: &mut AdminApiToken, updates: UpdateAdminTokenRequestBody) {
|
||||||
|
let params = token.params_mut().unwrap();
|
||||||
|
|
||||||
|
if let Some(name) = updates.name {
|
||||||
|
params.name.update(name);
|
||||||
|
}
|
||||||
|
if let Some(expiration) = updates.expiration {
|
||||||
|
params
|
||||||
|
.expiration
|
||||||
|
.update(Some(expiration.timestamp_millis() as u64));
|
||||||
|
}
|
||||||
|
if let Some(scope) = updates.scope {
|
||||||
|
params.scope.update(AdminApiTokenScope(scope));
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,13 @@ admin_endpoints![
|
||||||
GetClusterStatistics,
|
GetClusterStatistics,
|
||||||
ConnectClusterNodes,
|
ConnectClusterNodes,
|
||||||
|
|
||||||
|
// Admin tokens operations
|
||||||
|
ListAdminTokens,
|
||||||
|
GetAdminTokenInfo,
|
||||||
|
CreateAdminToken,
|
||||||
|
UpdateAdminToken,
|
||||||
|
DeleteAdminToken,
|
||||||
|
|
||||||
// Layout operations
|
// Layout operations
|
||||||
GetClusterLayout,
|
GetClusterLayout,
|
||||||
GetClusterLayoutHistory,
|
GetClusterLayoutHistory,
|
||||||
|
@ -282,6 +289,97 @@ pub struct ConnectNodeResponse {
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **********************************************
|
||||||
|
// Admin token operations
|
||||||
|
// **********************************************
|
||||||
|
|
||||||
|
// ---- ListAdminTokens ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ListAdminTokensRequest;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ListAdminTokensResponse(pub Vec<GetAdminTokenInfoResponse>);
|
||||||
|
|
||||||
|
// ---- GetAdminTokenInfo ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GetAdminTokenInfoRequest {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub search: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GetAdminTokenInfoResponse {
|
||||||
|
/// Identifier of the admin token (which is also a prefix of the full bearer token)
|
||||||
|
pub id: Option<String>,
|
||||||
|
/// Creation date
|
||||||
|
pub created: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
/// Name of the admin API token
|
||||||
|
pub name: String,
|
||||||
|
/// Expiration time and date, formatted according to RFC 3339
|
||||||
|
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
/// Whether this admin token is expired already
|
||||||
|
pub expired: bool,
|
||||||
|
/// Scope of the admin API token, a list of admin endpoint names (such as
|
||||||
|
/// `GetClusterStatus`, etc), or the special value `*` to allow all
|
||||||
|
/// admin endpoints
|
||||||
|
pub scope: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CreateAdminToken ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateAdminTokenRequest(pub UpdateAdminTokenRequestBody);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreateAdminTokenResponse {
|
||||||
|
/// The secret bearer token. **CAUTION:** This token will be shown only
|
||||||
|
/// ONCE, so this value MUST be remembered somewhere, or the token
|
||||||
|
/// will be unusable.
|
||||||
|
pub secret_token: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub info: GetAdminTokenInfoResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UpdateAdminToken ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateAdminTokenRequest {
|
||||||
|
pub id: String,
|
||||||
|
pub body: UpdateAdminTokenRequestBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateAdminTokenRequestBody {
|
||||||
|
/// Name of the admin API token
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// Expiration time and date, formatted according to RFC 3339
|
||||||
|
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
/// Scope of the admin API token, a list of admin endpoint names (such as
|
||||||
|
/// `GetClusterStatus`, etc), or the special value `*` to allow all
|
||||||
|
/// admin endpoints. **WARNING:** Granting a scope of `CreateAdminToken` or
|
||||||
|
/// `UpdateAdminToken` trivially allows for privilege escalation, and is thus
|
||||||
|
/// functionnally equivalent to granting a scope of `*`.
|
||||||
|
pub scope: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateAdminTokenResponse(pub GetAdminTokenInfoResponse);
|
||||||
|
|
||||||
|
// ---- DeleteAdminToken ----
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DeleteAdminTokenRequest {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DeleteAdminTokenResponse;
|
||||||
|
|
||||||
// **********************************************
|
// **********************************************
|
||||||
// Layout operations
|
// Layout operations
|
||||||
// **********************************************
|
// **********************************************
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use argon2::password_hash::PasswordHash;
|
|
||||||
|
|
||||||
use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION};
|
use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION};
|
||||||
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -15,10 +13,12 @@ use opentelemetry_prometheus::PrometheusExporter;
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_rpc::{Endpoint as RpcEndpoint, *};
|
use garage_rpc::{Endpoint as RpcEndpoint, *};
|
||||||
|
use garage_table::EmptyKey;
|
||||||
use garage_util::background::BackgroundRunner;
|
use garage_util::background::BackgroundRunner;
|
||||||
use garage_util::data::Uuid;
|
use garage_util::data::Uuid;
|
||||||
use garage_util::error::Error as GarageError;
|
use garage_util::error::Error as GarageError;
|
||||||
use garage_util::socket_address::UnixOrTCPSocketAddress;
|
use garage_util::socket_address::UnixOrTCPSocketAddress;
|
||||||
|
use garage_util::time::now_msec;
|
||||||
|
|
||||||
use garage_api_common::generic_server::*;
|
use garage_api_common::generic_server::*;
|
||||||
use garage_api_common::helpers::*;
|
use garage_api_common::helpers::*;
|
||||||
|
@ -99,6 +99,7 @@ pub struct AdminApiServer {
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
pub(crate) exporter: PrometheusExporter,
|
pub(crate) exporter: PrometheusExporter,
|
||||||
metrics_token: Option<String>,
|
metrics_token: Option<String>,
|
||||||
|
metrics_require_token: bool,
|
||||||
admin_token: Option<String>,
|
admin_token: Option<String>,
|
||||||
pub(crate) background: Arc<BackgroundRunner>,
|
pub(crate) background: Arc<BackgroundRunner>,
|
||||||
pub(crate) endpoint: Arc<RpcEndpoint<AdminRpc, Self>>,
|
pub(crate) endpoint: Arc<RpcEndpoint<AdminRpc, Self>>,
|
||||||
|
@ -118,6 +119,7 @@ impl AdminApiServer {
|
||||||
let cfg = &garage.config.admin;
|
let cfg = &garage.config.admin;
|
||||||
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
|
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
|
||||||
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
|
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
|
||||||
|
let metrics_require_token = cfg.metrics_require_token;
|
||||||
|
|
||||||
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
|
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
|
||||||
let admin = Arc::new(Self {
|
let admin = Arc::new(Self {
|
||||||
|
@ -125,6 +127,7 @@ impl AdminApiServer {
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
exporter,
|
exporter,
|
||||||
metrics_token,
|
metrics_token,
|
||||||
|
metrics_require_token,
|
||||||
admin_token,
|
admin_token,
|
||||||
background,
|
background,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
@ -156,25 +159,18 @@ impl AdminApiServer {
|
||||||
HttpEndpoint::New(_) => AdminApiRequest::from_request(req).await?,
|
HttpEndpoint::New(_) => AdminApiRequest::from_request(req).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let required_auth_hash =
|
let (global_token_hash, token_required) = match request.authorization_type() {
|
||||||
match request.authorization_type() {
|
Authorization::None => (None, false),
|
||||||
Authorization::None => None,
|
Authorization::MetricsToken => (
|
||||||
Authorization::MetricsToken => self.metrics_token.as_deref(),
|
self.metrics_token.as_deref(),
|
||||||
Authorization::AdminToken => match self.admin_token.as_deref() {
|
self.metrics_token.is_some() || self.metrics_require_token,
|
||||||
None => return Err(Error::forbidden(
|
),
|
||||||
"Admin token isn't configured, admin API access is disabled for security.",
|
Authorization::AdminToken => (self.admin_token.as_deref(), true),
|
||||||
)),
|
};
|
||||||
Some(t) => Some(t),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(password_hash) = required_auth_hash {
|
if token_required {
|
||||||
match auth_header {
|
verify_authorization(&self.garage, global_token_hash, auth_header, request.name())
|
||||||
None => return Err(Error::forbidden("Authorization token must be provided")),
|
.await?;
|
||||||
Some(authorization) => {
|
|
||||||
verify_bearer_token(&authorization, password_hash)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match request {
|
match request {
|
||||||
|
@ -249,20 +245,62 @@ fn hash_bearer_token(token: &str) -> String {
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_bearer_token(token: &hyper::http::HeaderValue, password_hash: &str) -> Result<(), Error> {
|
async fn verify_authorization(
|
||||||
use argon2::{password_hash::PasswordVerifier, Argon2};
|
garage: &Garage,
|
||||||
|
global_token_hash: Option<&str>,
|
||||||
|
auth_header: Option<hyper::http::HeaderValue>,
|
||||||
|
endpoint_name: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use argon2::{password_hash::PasswordHash, password_hash::PasswordVerifier, Argon2};
|
||||||
|
|
||||||
let parsed_hash = PasswordHash::new(&password_hash).unwrap();
|
let invalid_msg = "Invalid bearer token";
|
||||||
|
|
||||||
token
|
let token = match &auth_header {
|
||||||
.to_str()?
|
None => {
|
||||||
.strip_prefix("Bearer ")
|
return Err(Error::forbidden(
|
||||||
.and_then(|token| {
|
"Bearer token must be provided in Authorization header",
|
||||||
Argon2::default()
|
))
|
||||||
.verify_password(token.trim().as_bytes(), &parsed_hash)
|
}
|
||||||
.ok()
|
Some(authorization) => authorization
|
||||||
})
|
.to_str()?
|
||||||
.ok_or_else(|| Error::forbidden("Invalid authorization token"))?;
|
.strip_prefix("Bearer ")
|
||||||
|
.ok_or_else(|| Error::forbidden("Invalid Authorization header"))?
|
||||||
|
.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_hash_string = if let Some((prefix, _)) = token.split_once('.') {
|
||||||
|
garage
|
||||||
|
.admin_token_table
|
||||||
|
.get(&EmptyKey, &prefix.to_string())
|
||||||
|
.await?
|
||||||
|
.and_then(|k| k.state.into_option())
|
||||||
|
.filter(|p| {
|
||||||
|
p.expiration
|
||||||
|
.get()
|
||||||
|
.map(|exp| now_msec() < exp)
|
||||||
|
.unwrap_or(true)
|
||||||
|
})
|
||||||
|
.filter(|p| {
|
||||||
|
p.scope
|
||||||
|
.get()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.any(|x| x == "*" || x == endpoint_name)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||||
|
.token_hash
|
||||||
|
} else {
|
||||||
|
global_token_hash
|
||||||
|
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_hash =
|
||||||
|
PasswordHash::new(&token_hash_string).ok_or_internal_error("Could not parse token hash")?;
|
||||||
|
|
||||||
|
Argon2::default()
|
||||||
|
.verify_password(token.as_bytes(), &token_hash)
|
||||||
|
.map_err(|_| Error::forbidden(invalid_msg))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,15 +82,56 @@ impl RequestHandler for GetBucketInfoRequest {
|
||||||
let bucket_id = match (self.id, self.global_alias, self.search) {
|
let bucket_id = match (self.id, self.global_alias, self.search) {
|
||||||
(Some(id), None, None) => parse_bucket_id(&id)?,
|
(Some(id), None, None) => parse_bucket_id(&id)?,
|
||||||
(None, Some(ga), None) => garage
|
(None, Some(ga), None) => garage
|
||||||
.bucket_helper()
|
.bucket_alias_table
|
||||||
.resolve_global_bucket_name(&ga)
|
.get(&EmptyKey, &ga)
|
||||||
.await?
|
.await?
|
||||||
|
.and_then(|x| *x.state.get())
|
||||||
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
|
.ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?,
|
||||||
(None, None, Some(search)) => {
|
(None, None, Some(search)) => {
|
||||||
garage
|
let helper = garage.bucket_helper();
|
||||||
.bucket_helper()
|
if let Some(uuid) = helper.resolve_global_bucket_name(&search).await? {
|
||||||
.admin_get_existing_matching_bucket(&search)
|
uuid
|
||||||
.await?
|
} else {
|
||||||
|
let hexdec = if search.len() >= 2 {
|
||||||
|
search
|
||||||
|
.get(..search.len() & !1)
|
||||||
|
.and_then(|x| hex::decode(x).ok())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let hex = hexdec
|
||||||
|
.ok_or_else(|| Error::Common(CommonError::NoSuchBucket(search.clone())))?;
|
||||||
|
|
||||||
|
let mut start = [0u8; 32];
|
||||||
|
start
|
||||||
|
.as_mut_slice()
|
||||||
|
.get_mut(..hex.len())
|
||||||
|
.ok_or_bad_request("invalid length")?
|
||||||
|
.copy_from_slice(&hex);
|
||||||
|
let mut candidates = garage
|
||||||
|
.bucket_table
|
||||||
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
Some(start.into()),
|
||||||
|
Some(DeletedFilter::NotDeleted),
|
||||||
|
10,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
candidates.retain(|x| hex::encode(x.id).starts_with(&search));
|
||||||
|
if candidates.is_empty() {
|
||||||
|
return Err(Error::Common(CommonError::NoSuchBucket(search.clone())));
|
||||||
|
} else if candidates.len() == 1 {
|
||||||
|
candidates.into_iter().next().unwrap().id
|
||||||
|
} else {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"Several matching buckets: {}",
|
||||||
|
search
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::bad_request(
|
return Err(Error::bad_request(
|
||||||
|
|
|
@ -21,6 +21,10 @@ pub enum Error {
|
||||||
Common(#[error(source)] CommonError),
|
Common(#[error(source)] CommonError),
|
||||||
|
|
||||||
// Category: cannot process
|
// Category: cannot process
|
||||||
|
/// The admin API token does not exist
|
||||||
|
#[error(display = "Admin token not found: {}", _0)]
|
||||||
|
NoSuchAdminToken(String),
|
||||||
|
|
||||||
/// The API access key does not exist
|
/// The API access key does not exist
|
||||||
#[error(display = "Access key not found: {}", _0)]
|
#[error(display = "Access key not found: {}", _0)]
|
||||||
NoSuchAccessKey(String),
|
NoSuchAccessKey(String),
|
||||||
|
@ -60,6 +64,7 @@ impl Error {
|
||||||
pub fn code(&self) -> &'static str {
|
pub fn code(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Error::Common(c) => c.aws_code(),
|
Error::Common(c) => c.aws_code(),
|
||||||
|
Error::NoSuchAdminToken(_) => "NoSuchAdminToken",
|
||||||
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
|
Error::NoSuchAccessKey(_) => "NoSuchAccessKey",
|
||||||
Error::NoSuchWorker(_) => "NoSuchWorker",
|
Error::NoSuchWorker(_) => "NoSuchWorker",
|
||||||
Error::NoSuchBlock(_) => "NoSuchBlock",
|
Error::NoSuchBlock(_) => "NoSuchBlock",
|
||||||
|
@ -73,9 +78,10 @@ impl ApiError for Error {
|
||||||
fn http_status_code(&self) -> StatusCode {
|
fn http_status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Error::Common(c) => c.http_status_code(),
|
Error::Common(c) => c.http_status_code(),
|
||||||
Error::NoSuchAccessKey(_) | Error::NoSuchWorker(_) | Error::NoSuchBlock(_) => {
|
Error::NoSuchAdminToken(_)
|
||||||
StatusCode::NOT_FOUND
|
| Error::NoSuchAccessKey(_)
|
||||||
}
|
| Error::NoSuchWorker(_)
|
||||||
|
| Error::NoSuchBlock(_) => StatusCode::NOT_FOUND,
|
||||||
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,25 @@ impl RequestHandler for GetKeyInfoRequest {
|
||||||
let key = match (self.id, self.search) {
|
let key = match (self.id, self.search) {
|
||||||
(Some(id), None) => garage.key_helper().get_existing_key(&id).await?,
|
(Some(id), None) => garage.key_helper().get_existing_key(&id).await?,
|
||||||
(None, Some(search)) => {
|
(None, Some(search)) => {
|
||||||
garage
|
let candidates = garage
|
||||||
.key_helper()
|
.key_table
|
||||||
.get_existing_matching_key(&search)
|
.get_range(
|
||||||
|
&EmptyKey,
|
||||||
|
None,
|
||||||
|
Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
|
||||||
|
10,
|
||||||
|
EnumerationOrder::Forward,
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if candidates.len() != 1 {
|
||||||
|
return Err(Error::bad_request(format!(
|
||||||
|
"{} matching keys",
|
||||||
|
candidates.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
candidates.into_iter().next().unwrap()
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::bad_request(
|
return Err(Error::bad_request(
|
||||||
|
|
|
@ -11,6 +11,7 @@ mod router_v0;
|
||||||
mod router_v1;
|
mod router_v1;
|
||||||
mod router_v2;
|
mod router_v2;
|
||||||
|
|
||||||
|
mod admin_token;
|
||||||
mod bucket;
|
mod bucket;
|
||||||
mod cluster;
|
mod cluster;
|
||||||
mod key;
|
mod key;
|
||||||
|
|
|
@ -66,6 +66,82 @@ fn GetClusterStatistics() -> () {}
|
||||||
)]
|
)]
|
||||||
fn ConnectClusterNodes() -> () {}
|
fn ConnectClusterNodes() -> () {}
|
||||||
|
|
||||||
|
// **********************************************
|
||||||
|
// Admin API token operations
|
||||||
|
// **********************************************
|
||||||
|
|
||||||
|
#[utoipa::path(get,
|
||||||
|
path = "/v2/ListAdminTokens",
|
||||||
|
tag = "Admin API token",
|
||||||
|
description = "Returns all admin API tokens in the cluster.",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Returns info about all admin API tokens", body = ListAdminTokensResponse),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
fn ListAdminTokens() -> () {}
|
||||||
|
|
||||||
|
#[utoipa::path(get,
|
||||||
|
path = "/v2/GetAdminTokenInfo",
|
||||||
|
tag = "Admin API token",
|
||||||
|
description = "
|
||||||
|
Return information about a specific admin API token.
|
||||||
|
You can search by specifying the exact token identifier (`id`) or by specifying a pattern (`search`).
|
||||||
|
",
|
||||||
|
params(
|
||||||
|
("id", description = "Admin API token ID"),
|
||||||
|
("search", description = "Partial token ID or name to search for"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Information about the admin token", body = GetAdminTokenInfoResponse),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
fn GetAdminTokenInfo() -> () {}
|
||||||
|
|
||||||
|
#[utoipa::path(post,
|
||||||
|
path = "/v2/CreateAdminToken",
|
||||||
|
tag = "Admin API token",
|
||||||
|
description = "Creates a new admin API token",
|
||||||
|
request_body = UpdateAdminTokenRequestBody,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin token has been created", body = CreateAdminTokenResponse),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
fn CreateAdminToken() -> () {}
|
||||||
|
|
||||||
|
#[utoipa::path(post,
|
||||||
|
path = "/v2/UpdateAdminToken",
|
||||||
|
tag = "Admin API token",
|
||||||
|
description = "
|
||||||
|
Updates information about the specified admin API token.
|
||||||
|
",
|
||||||
|
request_body = UpdateAdminTokenRequestBody,
|
||||||
|
params(
|
||||||
|
("id", description = "Admin API token ID"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin token has been updated", body = UpdateAdminTokenResponse),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
fn UpdateAdminToken() -> () {}
|
||||||
|
|
||||||
|
#[utoipa::path(post,
|
||||||
|
path = "/v2/DeleteAdminToken",
|
||||||
|
tag = "Admin API token",
|
||||||
|
description = "Delete an admin API token from the cluster, revoking all its permissions.",
|
||||||
|
params(
|
||||||
|
("id", description = "Admin API token ID"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Admin token has been deleted"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
fn DeleteAdminToken() -> () {}
|
||||||
|
|
||||||
// **********************************************
|
// **********************************************
|
||||||
// Layout operations
|
// Layout operations
|
||||||
// **********************************************
|
// **********************************************
|
||||||
|
@ -723,6 +799,12 @@ impl Modify for SecurityAddon {
|
||||||
GetClusterStatus,
|
GetClusterStatus,
|
||||||
GetClusterStatistics,
|
GetClusterStatistics,
|
||||||
ConnectClusterNodes,
|
ConnectClusterNodes,
|
||||||
|
// Admin token operations
|
||||||
|
ListAdminTokens,
|
||||||
|
GetAdminTokenInfo,
|
||||||
|
CreateAdminToken,
|
||||||
|
UpdateAdminToken,
|
||||||
|
DeleteAdminToken,
|
||||||
// Layout operations
|
// Layout operations
|
||||||
GetClusterLayout,
|
GetClusterLayout,
|
||||||
GetClusterLayoutHistory,
|
GetClusterLayoutHistory,
|
||||||
|
|
|
@ -34,6 +34,12 @@ impl AdminApiRequest {
|
||||||
GET GetClusterStatus (),
|
GET GetClusterStatus (),
|
||||||
GET GetClusterHealth (),
|
GET GetClusterHealth (),
|
||||||
POST ConnectClusterNodes (body),
|
POST ConnectClusterNodes (body),
|
||||||
|
// Admin token endpoints
|
||||||
|
GET ListAdminTokens (),
|
||||||
|
GET GetAdminTokenInfo (query_opt::id, query_opt::search),
|
||||||
|
POST CreateAdminToken (body),
|
||||||
|
POST UpdateAdminToken (body_field, query::id),
|
||||||
|
POST DeleteAdminToken (query::id),
|
||||||
// Layout endpoints
|
// Layout endpoints
|
||||||
GET GetClusterLayout (),
|
GET GetClusterLayout (),
|
||||||
GET GetClusterLayoutHistory (),
|
GET GetClusterLayoutHistory (),
|
||||||
|
@ -243,9 +249,7 @@ impl AdminApiRequest {
|
||||||
/// Get the kind of authorization which is required to perform the operation.
|
/// Get the kind of authorization which is required to perform the operation.
|
||||||
pub fn authorization_type(&self) -> Authorization {
|
pub fn authorization_type(&self) -> Authorization {
|
||||||
match self {
|
match self {
|
||||||
Self::Options(_) => Authorization::None,
|
Self::Options(_) | Self::Health(_) | Self::CheckDomain(_) => Authorization::None,
|
||||||
Self::Health(_) => Authorization::None,
|
|
||||||
Self::CheckDomain(_) => Authorization::None,
|
|
||||||
Self::Metrics(_) => Authorization::MetricsToken,
|
Self::Metrics(_) => Authorization::MetricsToken,
|
||||||
_ => Authorization::AdminToken,
|
_ => Authorization::AdminToken,
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ garage_web.workspace = true
|
||||||
backtrace.workspace = true
|
backtrace.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
timeago.workspace = true
|
timeago.workspace = true
|
||||||
parse_duration.workspace = true
|
parse_duration.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
|
253
src/garage/cli/remote/admin_token.rs
Normal file
253
src/garage/cli/remote/admin_token.rs
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
use format_table::format_table;
|
||||||
|
|
||||||
|
use chrono::{Local, Utc};
|
||||||
|
|
||||||
|
use garage_util::error::*;
|
||||||
|
|
||||||
|
use garage_api_admin::api::*;
|
||||||
|
|
||||||
|
use crate::cli::remote::*;
|
||||||
|
use crate::cli::structs::*;
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
pub async fn cmd_admin_token(&self, cmd: AdminTokenOperation) -> Result<(), Error> {
|
||||||
|
match cmd {
|
||||||
|
AdminTokenOperation::List => self.cmd_list_admin_tokens().await,
|
||||||
|
AdminTokenOperation::Info { api_token } => self.cmd_admin_token_info(api_token).await,
|
||||||
|
AdminTokenOperation::Create(opt) => self.cmd_create_admin_token(opt).await,
|
||||||
|
AdminTokenOperation::Rename {
|
||||||
|
api_token,
|
||||||
|
new_name,
|
||||||
|
} => self.cmd_rename_admin_token(api_token, new_name).await,
|
||||||
|
AdminTokenOperation::Set(opt) => self.cmd_update_admin_token(opt).await,
|
||||||
|
AdminTokenOperation::Delete { api_token, yes } => {
|
||||||
|
self.cmd_delete_admin_token(api_token, yes).await
|
||||||
|
}
|
||||||
|
AdminTokenOperation::DeleteExpired { yes } => {
|
||||||
|
self.cmd_delete_expired_admin_tokens(yes).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_list_admin_tokens(&self) -> Result<(), Error> {
|
||||||
|
let mut list = self.api_request(ListAdminTokensRequest).await?;
|
||||||
|
|
||||||
|
list.0.sort_by_key(|x| x.created);
|
||||||
|
|
||||||
|
let mut table = vec!["ID\tCREATED\tNAME\tEXPIRATION\tSCOPE".to_string()];
|
||||||
|
for tok in list.0.iter() {
|
||||||
|
let scope = if tok.expired {
|
||||||
|
String::new()
|
||||||
|
} else if tok.scope.len() > 1 {
|
||||||
|
format!("[{}]", tok.scope.len())
|
||||||
|
} else {
|
||||||
|
tok.scope.get(0).cloned().unwrap_or_default()
|
||||||
|
};
|
||||||
|
let exp = if tok.expired {
|
||||||
|
"expired".to_string()
|
||||||
|
} else {
|
||||||
|
tok.expiration
|
||||||
|
.map(|x| x.with_timezone(&Local).to_string())
|
||||||
|
.unwrap_or("never".into())
|
||||||
|
};
|
||||||
|
table.push(format!(
|
||||||
|
"{}\t{}\t{}\t{}\t{}",
|
||||||
|
tok.id.as_deref().unwrap_or("-"),
|
||||||
|
tok.created
|
||||||
|
.map(|x| x.with_timezone(&Local).date_naive().to_string())
|
||||||
|
.unwrap_or("-".into()),
|
||||||
|
tok.name,
|
||||||
|
exp,
|
||||||
|
scope,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
format_table(table);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_admin_token_info(&self, search: String) -> Result<(), Error> {
|
||||||
|
let info = self
|
||||||
|
.api_request(GetAdminTokenInfoRequest {
|
||||||
|
id: None,
|
||||||
|
search: Some(search),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
print_token_info(&info);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_create_admin_token(&self, opt: AdminTokenCreateOp) -> Result<(), Error> {
|
||||||
|
// TODO
|
||||||
|
let res = self
|
||||||
|
.api_request(CreateAdminTokenRequest(UpdateAdminTokenRequestBody {
|
||||||
|
name: opt.name,
|
||||||
|
expiration: opt
|
||||||
|
.expires_in
|
||||||
|
.map(|x| parse_duration::parse::parse(&x))
|
||||||
|
.transpose()
|
||||||
|
.ok_or_message("Invalid duration passed for --expires-in parameter")?
|
||||||
|
.map(|dur| Utc::now() + dur),
|
||||||
|
scope: opt.scope.map(|s| {
|
||||||
|
s.split(",")
|
||||||
|
.map(|x| x.trim().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if opt.quiet {
|
||||||
|
println!("{}", res.secret_token);
|
||||||
|
} else {
|
||||||
|
println!("This is your secret bearer token, it will not be shown again by Garage:");
|
||||||
|
println!("\n {}\n", res.secret_token);
|
||||||
|
print_token_info(&res.info);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_rename_admin_token(&self, old: String, new: String) -> Result<(), Error> {
|
||||||
|
let token = self
|
||||||
|
.api_request(GetAdminTokenInfoRequest {
|
||||||
|
id: None,
|
||||||
|
search: Some(old),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let info = self
|
||||||
|
.api_request(UpdateAdminTokenRequest {
|
||||||
|
id: token.id.unwrap(),
|
||||||
|
body: UpdateAdminTokenRequestBody {
|
||||||
|
name: Some(new),
|
||||||
|
expiration: None,
|
||||||
|
scope: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
print_token_info(&info.0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_update_admin_token(&self, opt: AdminTokenSetOp) -> Result<(), Error> {
|
||||||
|
let token = self
|
||||||
|
.api_request(GetAdminTokenInfoRequest {
|
||||||
|
id: None,
|
||||||
|
search: Some(opt.api_token),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let info = self
|
||||||
|
.api_request(UpdateAdminTokenRequest {
|
||||||
|
id: token.id.unwrap(),
|
||||||
|
body: UpdateAdminTokenRequestBody {
|
||||||
|
name: None,
|
||||||
|
expiration: opt
|
||||||
|
.expires_in
|
||||||
|
.map(|x| parse_duration::parse::parse(&x))
|
||||||
|
.transpose()
|
||||||
|
.ok_or_message("Invalid duration passed for --expires-in parameter")?
|
||||||
|
.map(|dur| Utc::now() + dur),
|
||||||
|
scope: opt.scope.map({
|
||||||
|
let mut new_scope = token.scope;
|
||||||
|
|scope_str| {
|
||||||
|
if let Some(add) = scope_str.strip_prefix("+") {
|
||||||
|
for a in add.split(",").map(|x| x.trim().to_string()) {
|
||||||
|
if !new_scope.contains(&a) {
|
||||||
|
new_scope.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_scope
|
||||||
|
} else if let Some(sub) = scope_str.strip_prefix("-") {
|
||||||
|
for r in sub.split(",").map(|x| x.trim()) {
|
||||||
|
new_scope.retain(|x| x != r);
|
||||||
|
}
|
||||||
|
new_scope
|
||||||
|
} else {
|
||||||
|
scope_str
|
||||||
|
.split(",")
|
||||||
|
.map(|x| x.trim().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
print_token_info(&info.0);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_delete_admin_token(&self, token: String, yes: bool) -> Result<(), Error> {
|
||||||
|
let token = self
|
||||||
|
.api_request(GetAdminTokenInfoRequest {
|
||||||
|
id: None,
|
||||||
|
search: Some(token),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let id = token.id.unwrap();
|
||||||
|
|
||||||
|
if !yes {
|
||||||
|
return Err(Error::Message(format!(
|
||||||
|
"Add the --yes flag to delete API token `{}` ({})",
|
||||||
|
token.name, id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.api_request(DeleteAdminTokenRequest { id }).await?;
|
||||||
|
|
||||||
|
println!("Admin API token has been deleted.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_delete_expired_admin_tokens(&self, yes: bool) -> Result<(), Error> {
|
||||||
|
let mut list = self.api_request(ListAdminTokensRequest).await?.0;
|
||||||
|
|
||||||
|
list.retain(|tok| tok.expired);
|
||||||
|
|
||||||
|
if !yes {
|
||||||
|
return Err(Error::Message(format!(
|
||||||
|
"This would delete {} admin API tokens, add the --yes flag to proceed.",
|
||||||
|
list.len(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for token in list.iter() {
|
||||||
|
let id = token.id.clone().unwrap();
|
||||||
|
println!("Deleting token `{}` ({})", token.name, id);
|
||||||
|
self.api_request(DeleteAdminTokenRequest { id }).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{} admin API tokens have been deleted.", list.len());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_token_info(token: &GetAdminTokenInfoResponse) {
|
||||||
|
format_table(vec![
|
||||||
|
format!("ID:\t{}", token.id.as_ref().unwrap()),
|
||||||
|
format!("Name:\t{}", token.name),
|
||||||
|
format!("Created:\t{}", token.created.unwrap().with_timezone(&Local)),
|
||||||
|
format!(
|
||||||
|
"Validity:\t{}",
|
||||||
|
token.expired.then_some("EXPIRED").unwrap_or("valid")
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"Expiration:\t{}",
|
||||||
|
token
|
||||||
|
.expiration
|
||||||
|
.map(|x| x.with_timezone(&Local).to_string())
|
||||||
|
.unwrap_or("never".into())
|
||||||
|
),
|
||||||
|
format!("Scope:\t{}", token.scope.to_vec().join(", ")),
|
||||||
|
]);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod admin_token;
|
||||||
pub mod bucket;
|
pub mod bucket;
|
||||||
pub mod cluster;
|
pub mod cluster;
|
||||||
pub mod key;
|
pub mod key;
|
||||||
|
@ -35,6 +36,7 @@ impl Cli {
|
||||||
}
|
}
|
||||||
Command::Layout(layout_opt) => self.layout_command_dispatch(layout_opt).await,
|
Command::Layout(layout_opt) => self.layout_command_dispatch(layout_opt).await,
|
||||||
Command::Bucket(bo) => self.cmd_bucket(bo).await,
|
Command::Bucket(bo) => self.cmd_bucket(bo).await,
|
||||||
|
Command::AdminToken(to) => self.cmd_admin_token(to).await,
|
||||||
Command::Key(ko) => self.cmd_key(ko).await,
|
Command::Key(ko) => self.cmd_key(ko).await,
|
||||||
Command::Worker(wo) => self.cmd_worker(wo).await,
|
Command::Worker(wo) => self.cmd_worker(wo).await,
|
||||||
Command::Block(bo) => self.cmd_block(bo).await,
|
Command::Block(bo) => self.cmd_block(bo).await,
|
||||||
|
|
|
@ -30,6 +30,10 @@ pub enum Command {
|
||||||
#[structopt(name = "key", version = garage_version())]
|
#[structopt(name = "key", version = garage_version())]
|
||||||
Key(KeyOperation),
|
Key(KeyOperation),
|
||||||
|
|
||||||
|
/// Operations on admin API tokens
|
||||||
|
#[structopt(name = "admin-token", version = garage_version())]
|
||||||
|
AdminToken(AdminTokenOperation),
|
||||||
|
|
||||||
/// Start repair of node data on remote node
|
/// Start repair of node data on remote node
|
||||||
#[structopt(name = "repair", version = garage_version())]
|
#[structopt(name = "repair", version = garage_version())]
|
||||||
Repair(RepairOpt),
|
Repair(RepairOpt),
|
||||||
|
@ -64,6 +68,10 @@ pub enum Command {
|
||||||
AdminApiSchema,
|
AdminApiSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// ---- garage node ... ----
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
pub enum NodeOperation {
|
pub enum NodeOperation {
|
||||||
/// Print the full node ID (public key) of this Garage node, and its publicly reachable IP
|
/// Print the full node ID (public key) of this Garage node, and its publicly reachable IP
|
||||||
|
@ -91,6 +99,10 @@ pub struct ConnectNodeOpt {
|
||||||
pub(crate) node: String,
|
pub(crate) node: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// ---- garage layout ... ----
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
pub enum LayoutOperation {
|
pub enum LayoutOperation {
|
||||||
/// Assign role to Garage node
|
/// Assign role to Garage node
|
||||||
|
@ -193,6 +205,10 @@ pub struct SkipDeadNodesOpt {
|
||||||
pub(crate) allow_missing_data: bool,
|
pub(crate) allow_missing_data: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// ---- garage bucket ... ----
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
pub enum BucketOperation {
|
pub enum BucketOperation {
|
||||||
/// List buckets
|
/// List buckets
|
||||||
|
@ -350,6 +366,10 @@ pub struct CleanupIncompleteUploadsOpt {
|
||||||
pub buckets: Vec<String>,
|
pub buckets: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
// ---- garage key ... ----
|
||||||
|
// ------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
pub enum KeyOperation {
|
pub enum KeyOperation {
|
||||||
/// List keys
|
/// List keys
|
||||||
|
@ -447,6 +467,104 @@ pub struct KeyImportOpt {
|
||||||
pub yes: bool,
|
pub yes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
// ---- garage admin-token ... ----
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
pub enum AdminTokenOperation {
|
||||||
|
/// List all admin API tokens
|
||||||
|
#[structopt(name = "list", version = garage_version())]
|
||||||
|
List,
|
||||||
|
|
||||||
|
/// Fetch info about a specific admin API token
|
||||||
|
#[structopt(name = "info", version = garage_version())]
|
||||||
|
Info {
|
||||||
|
/// Name or prefix of the ID of the token to look up
|
||||||
|
api_token: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Create new admin API token
|
||||||
|
#[structopt(name = "create", version = garage_version())]
|
||||||
|
Create(AdminTokenCreateOp),
|
||||||
|
|
||||||
|
/// Rename an admin API token
|
||||||
|
#[structopt(name = "rename", version = garage_version())]
|
||||||
|
Rename {
|
||||||
|
/// Name or prefix of the ID of the token to rename
|
||||||
|
api_token: String,
|
||||||
|
/// New name of the admintoken
|
||||||
|
new_name: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Set parameters for an admin API token
|
||||||
|
#[structopt(name = "set", version = garage_version())]
|
||||||
|
Set(AdminTokenSetOp),
|
||||||
|
|
||||||
|
/// Delete an admin API token
|
||||||
|
#[structopt(name = "delete", version = garage_version())]
|
||||||
|
Delete {
|
||||||
|
/// Name or prefix of the ID of the token to delete
|
||||||
|
api_token: String,
|
||||||
|
/// Confirm deletion
|
||||||
|
#[structopt(long = "yes")]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Delete all expired admin API tokens
|
||||||
|
#[structopt(name = "delete-expired", version = garage_version())]
|
||||||
|
DeleteExpired {
|
||||||
|
/// Confirm deletion
|
||||||
|
#[structopt(long = "yes")]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug, Clone)]
|
||||||
|
pub struct AdminTokenCreateOp {
|
||||||
|
/// Set a name for the token
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// Set an expiration time for the token (see docs.rs/parse_duration for date
|
||||||
|
/// format)
|
||||||
|
#[structopt(long = "expires-in")]
|
||||||
|
pub expires_in: Option<String>,
|
||||||
|
/// Set a limited scope for the token, as a comma-separated list of
|
||||||
|
/// admin API functions (e.g. GetClusterStatus, etc.). The default scope
|
||||||
|
/// is `*`, which allows access to all admin API functions.
|
||||||
|
/// Note that granting a scope that allows `CreateAdminToken` or
|
||||||
|
/// `UpdateAdminToken` allows for privilege escalation, and is therefore
|
||||||
|
/// equivalent to `*`.
|
||||||
|
#[structopt(long = "scope")]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
/// Print only the newly generated API token to stdout
|
||||||
|
#[structopt(short = "q", long = "quiet")]
|
||||||
|
pub quiet: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug, Clone)]
|
||||||
|
pub struct AdminTokenSetOp {
|
||||||
|
/// Name or prefix of the ID of the token to modify
|
||||||
|
pub api_token: String,
|
||||||
|
/// Set an expiration time for the token (see docs.rs/parse_duration for date
|
||||||
|
/// format)
|
||||||
|
#[structopt(long = "expires-in")]
|
||||||
|
pub expires_in: Option<String>,
|
||||||
|
/// Set a limited scope for the token, as a comma-separated list of
|
||||||
|
/// admin API functions (e.g. GetClusterStatus, etc.), or `*` to allow
|
||||||
|
/// all admin API functions.
|
||||||
|
/// Use `--scope=+Scope1,Scope2` to add scopes to the existing list,
|
||||||
|
/// and `--scope=-Scope1,Scope2` to remove scopes from the existing list.
|
||||||
|
/// Note that granting a scope that allows `CreateAdminToken` or
|
||||||
|
/// `UpdateAdminToken` allows for privilege escalation, and is therefore
|
||||||
|
/// equivalent to `*`.
|
||||||
|
#[structopt(long = "scope")]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// ---- garage repair ... ----
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug, Clone)]
|
#[derive(StructOpt, Debug, Clone)]
|
||||||
pub struct RepairOpt {
|
pub struct RepairOpt {
|
||||||
/// Launch repair operation on all nodes
|
/// Launch repair operation on all nodes
|
||||||
|
@ -508,6 +626,10 @@ pub enum ScrubCmd {
|
||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// ---- garage offline-repair ... ----
|
||||||
|
// -----------------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug, Clone)]
|
#[derive(StructOpt, Debug, Clone)]
|
||||||
pub struct OfflineRepairOpt {
|
pub struct OfflineRepairOpt {
|
||||||
/// Confirm the launch of the repair operation
|
/// Confirm the launch of the repair operation
|
||||||
|
@ -529,6 +651,10 @@ pub enum OfflineRepairWhat {
|
||||||
ObjectCounters,
|
ObjectCounters,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// ---- garage stats ... ----
|
||||||
|
// --------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug, Clone)]
|
#[derive(StructOpt, Debug, Clone)]
|
||||||
pub struct StatsOpt {
|
pub struct StatsOpt {
|
||||||
/// Gather statistics from all nodes
|
/// Gather statistics from all nodes
|
||||||
|
@ -536,6 +662,10 @@ pub struct StatsOpt {
|
||||||
pub all_nodes: bool,
|
pub all_nodes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// ---- garage worker ... ----
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug, Eq, PartialEq, Clone)]
|
#[derive(StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||||
pub enum WorkerOperation {
|
pub enum WorkerOperation {
|
||||||
/// List all workers on Garage node
|
/// List all workers on Garage node
|
||||||
|
@ -579,6 +709,10 @@ pub struct WorkerListOpt {
|
||||||
pub errors: bool,
|
pub errors: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// ---- garage block ... ----
|
||||||
|
// --------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug, Eq, PartialEq, Clone)]
|
#[derive(StructOpt, Debug, Eq, PartialEq, Clone)]
|
||||||
pub enum BlockOperation {
|
pub enum BlockOperation {
|
||||||
/// List all blocks that currently have a resync error
|
/// List all blocks that currently have a resync error
|
||||||
|
@ -611,6 +745,10 @@ pub enum BlockOperation {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// ---- garage meta ... ----
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
#[derive(StructOpt, Debug, Eq, PartialEq, Clone, Copy)]
|
#[derive(StructOpt, Debug, Eq, PartialEq, Clone, Copy)]
|
||||||
pub enum MetaOperation {
|
pub enum MetaOperation {
|
||||||
/// Save a snapshot of the metadata db file
|
/// Save a snapshot of the metadata db file
|
||||||
|
|
|
@ -21,6 +21,7 @@ garage_block.workspace = true
|
||||||
garage_util.workspace = true
|
garage_util.workspace = true
|
||||||
garage_net.workspace = true
|
garage_net.workspace = true
|
||||||
|
|
||||||
|
argon2.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
blake2.workspace = true
|
blake2.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
|
177
src/model/admin_token_table.rs
Normal file
177
src/model/admin_token_table.rs
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
use base64::prelude::*;
|
||||||
|
|
||||||
|
use garage_util::crdt::{self, Crdt};
|
||||||
|
use garage_util::time::now_msec;
|
||||||
|
|
||||||
|
use garage_table::{EmptyKey, Entry, TableSchema};
|
||||||
|
|
||||||
|
pub use crate::key_table::KeyFilter;
|
||||||
|
|
||||||
|
mod v2 {
|
||||||
|
use garage_util::crdt;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AdminApiToken {
|
||||||
|
/// An admin API token is a bearer token of the following form:
|
||||||
|
/// `<prefix>.<suffix>`
|
||||||
|
/// Only the prefix is saved here, it is used as an identifier.
|
||||||
|
/// The entire API token is hashed and saved in `token_hash` in `state`.
|
||||||
|
pub prefix: String,
|
||||||
|
|
||||||
|
/// If the token is not deleted, its parameters
|
||||||
|
pub state: crdt::Deletable<AdminApiTokenParams>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AdminApiTokenParams {
|
||||||
|
/// Creation date
|
||||||
|
pub created: u64,
|
||||||
|
|
||||||
|
/// The entire API token hashed as a password
|
||||||
|
pub token_hash: String,
|
||||||
|
|
||||||
|
/// User-defined name
|
||||||
|
pub name: crdt::Lww<String>,
|
||||||
|
|
||||||
|
/// The optional time of expiration of the token
|
||||||
|
pub expiration: crdt::Lww<Option<u64>>,
|
||||||
|
|
||||||
|
/// The scope of the token, i.e. list of authorized admin API calls
|
||||||
|
pub scope: crdt::Lww<AdminApiTokenScope>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AdminApiTokenScope(pub Vec<String>);
|
||||||
|
|
||||||
|
impl garage_util::migrate::InitialFormat for AdminApiToken {
|
||||||
|
const VERSION_MARKER: &'static [u8] = b"G2admtok";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use v2::*;
|
||||||
|
|
||||||
|
impl Crdt for AdminApiTokenParams {
|
||||||
|
fn merge(&mut self, o: &Self) {
|
||||||
|
self.name.merge(&o.name);
|
||||||
|
self.expiration.merge(&o.expiration);
|
||||||
|
self.scope.merge(&o.scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crdt for AdminApiToken {
|
||||||
|
fn merge(&mut self, other: &Self) {
|
||||||
|
self.state.merge(&other.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crdt for AdminApiTokenScope {
|
||||||
|
fn merge(&mut self, other: &Self) {
|
||||||
|
self.0.retain(|x| other.0.contains(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminApiToken {
|
||||||
|
/// Create a new admin API token.
|
||||||
|
/// Returns the AdminApiToken object, which contains the hashed bearer token,
|
||||||
|
/// as well as the plaintext bearer token.
|
||||||
|
pub fn new(name: &str) -> (Self, String) {
|
||||||
|
use argon2::{
|
||||||
|
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prefix = hex::encode(&rand::random::<[u8; 12]>()[..]);
|
||||||
|
let secret = BASE64_URL_SAFE_NO_PAD.encode(&rand::random::<[u8; 32]>()[..]);
|
||||||
|
let token = format!("{}.{}", prefix, secret);
|
||||||
|
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let hashed_token = argon2
|
||||||
|
.hash_password(token.as_bytes(), &salt)
|
||||||
|
.expect("could not hash admin API token")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let ret = AdminApiToken {
|
||||||
|
prefix,
|
||||||
|
state: crdt::Deletable::present(AdminApiTokenParams {
|
||||||
|
created: now_msec(),
|
||||||
|
token_hash: hashed_token,
|
||||||
|
name: crdt::Lww::new(name.to_string()),
|
||||||
|
expiration: crdt::Lww::new(None),
|
||||||
|
scope: crdt::Lww::new(AdminApiTokenScope(vec!["*".to_string()])),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
(ret, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(prefix: String) -> Self {
|
||||||
|
Self {
|
||||||
|
prefix,
|
||||||
|
state: crdt::Deletable::Deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this represents a deleted bucket
|
||||||
|
pub fn is_deleted(&self) -> bool {
|
||||||
|
self.state.is_deleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an option representing the params (None if in deleted state)
|
||||||
|
pub fn params(&self) -> Option<&AdminApiTokenParams> {
|
||||||
|
self.state.as_option()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutable version of `.state()`
|
||||||
|
pub fn params_mut(&mut self) -> Option<&mut AdminApiTokenParams> {
|
||||||
|
self.state.as_option_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scope, if not deleted, or empty slice
|
||||||
|
pub fn scope(&self) -> &[String] {
|
||||||
|
self.state
|
||||||
|
.as_option()
|
||||||
|
.map(|x| &x.scope.get().0[..])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry<EmptyKey, String> for AdminApiToken {
|
||||||
|
fn partition_key(&self) -> &EmptyKey {
|
||||||
|
&EmptyKey
|
||||||
|
}
|
||||||
|
fn sort_key(&self) -> &String {
|
||||||
|
&self.prefix
|
||||||
|
}
|
||||||
|
fn is_tombstone(&self) -> bool {
|
||||||
|
self.is_deleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AdminApiTokenTable;
|
||||||
|
|
||||||
|
impl TableSchema for AdminApiTokenTable {
|
||||||
|
const TABLE_NAME: &'static str = "admin_token";
|
||||||
|
|
||||||
|
type P = EmptyKey;
|
||||||
|
type S = String;
|
||||||
|
type E = AdminApiToken;
|
||||||
|
type Filter = KeyFilter;
|
||||||
|
|
||||||
|
fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool {
|
||||||
|
match filter {
|
||||||
|
KeyFilter::Deleted(df) => df.apply(entry.state.is_deleted()),
|
||||||
|
KeyFilter::MatchesAndNotDeleted(pat) => {
|
||||||
|
let pat = pat.to_lowercase();
|
||||||
|
entry
|
||||||
|
.params()
|
||||||
|
.map(|p| {
|
||||||
|
entry.prefix.to_lowercase().starts_with(&pat)
|
||||||
|
|| p.name.get().to_lowercase() == pat
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ use crate::s3::mpu_table::*;
|
||||||
use crate::s3::object_table::*;
|
use crate::s3::object_table::*;
|
||||||
use crate::s3::version_table::*;
|
use crate::s3::version_table::*;
|
||||||
|
|
||||||
|
use crate::admin_token_table::*;
|
||||||
use crate::bucket_alias_table::*;
|
use crate::bucket_alias_table::*;
|
||||||
use crate::bucket_table::*;
|
use crate::bucket_table::*;
|
||||||
use crate::helper;
|
use crate::helper;
|
||||||
|
@ -50,6 +51,8 @@ pub struct Garage {
|
||||||
/// The block manager
|
/// The block manager
|
||||||
pub block_manager: Arc<BlockManager>,
|
pub block_manager: Arc<BlockManager>,
|
||||||
|
|
||||||
|
/// Table containing admin API keys
|
||||||
|
pub admin_token_table: Arc<Table<AdminApiTokenTable, TableFullReplication>>,
|
||||||
/// Table containing buckets
|
/// Table containing buckets
|
||||||
pub bucket_table: Arc<Table<BucketTable, TableFullReplication>>,
|
pub bucket_table: Arc<Table<BucketTable, TableFullReplication>>,
|
||||||
/// Table containing bucket aliases
|
/// Table containing bucket aliases
|
||||||
|
@ -174,6 +177,14 @@ impl Garage {
|
||||||
block_manager.register_bg_vars(&mut bg_vars);
|
block_manager.register_bg_vars(&mut bg_vars);
|
||||||
|
|
||||||
// ---- admin tables ----
|
// ---- admin tables ----
|
||||||
|
info!("Initialize admin_token_table...");
|
||||||
|
let admin_token_table = Table::new(
|
||||||
|
AdminApiTokenTable,
|
||||||
|
control_rep_param.clone(),
|
||||||
|
system.clone(),
|
||||||
|
&db,
|
||||||
|
);
|
||||||
|
|
||||||
info!("Initialize bucket_table...");
|
info!("Initialize bucket_table...");
|
||||||
let bucket_table = Table::new(BucketTable, control_rep_param.clone(), system.clone(), &db);
|
let bucket_table = Table::new(BucketTable, control_rep_param.clone(), system.clone(), &db);
|
||||||
|
|
||||||
|
@ -263,6 +274,7 @@ impl Garage {
|
||||||
db,
|
db,
|
||||||
system,
|
system,
|
||||||
block_manager,
|
block_manager,
|
||||||
|
admin_token_table,
|
||||||
bucket_table,
|
bucket_table,
|
||||||
bucket_alias_table,
|
bucket_alias_table,
|
||||||
key_table,
|
key_table,
|
||||||
|
@ -282,6 +294,7 @@ impl Garage {
|
||||||
pub fn spawn_workers(self: &Arc<Self>, bg: &BackgroundRunner) -> Result<(), Error> {
|
pub fn spawn_workers(self: &Arc<Self>, bg: &BackgroundRunner) -> Result<(), Error> {
|
||||||
self.block_manager.spawn_workers(bg);
|
self.block_manager.spawn_workers(bg);
|
||||||
|
|
||||||
|
self.admin_token_table.spawn_workers(bg);
|
||||||
self.bucket_table.spawn_workers(bg);
|
self.bucket_table.spawn_workers(bg);
|
||||||
self.bucket_alias_table.spawn_workers(bg);
|
self.bucket_alias_table.spawn_workers(bg);
|
||||||
self.key_table.spawn_workers(bg);
|
self.key_table.spawn_workers(bg);
|
||||||
|
|
|
@ -67,56 +67,6 @@ impl<'a> BucketHelper<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a bucket by its global alias or a prefix of its uuid
|
|
||||||
pub async fn admin_get_existing_matching_bucket(
|
|
||||||
&self,
|
|
||||||
pattern: &String,
|
|
||||||
) -> Result<Uuid, Error> {
|
|
||||||
if let Some(uuid) = self.resolve_global_bucket_name(pattern).await? {
|
|
||||||
Ok(uuid)
|
|
||||||
} else {
|
|
||||||
let hexdec = if pattern.len() >= 2 {
|
|
||||||
pattern
|
|
||||||
.get(..pattern.len() & !1)
|
|
||||||
.and_then(|x| hex::decode(x).ok())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let hex = hexdec.ok_or_else(|| Error::NoSuchBucket(pattern.clone()))?;
|
|
||||||
|
|
||||||
let mut start = [0u8; 32];
|
|
||||||
start
|
|
||||||
.as_mut_slice()
|
|
||||||
.get_mut(..hex.len())
|
|
||||||
.ok_or_bad_request("invalid length")?
|
|
||||||
.copy_from_slice(&hex);
|
|
||||||
let mut candidates = self
|
|
||||||
.0
|
|
||||||
.bucket_table
|
|
||||||
.get_range(
|
|
||||||
&EmptyKey,
|
|
||||||
Some(start.into()),
|
|
||||||
Some(DeletedFilter::NotDeleted),
|
|
||||||
10,
|
|
||||||
EnumerationOrder::Forward,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
candidates.retain(|x| hex::encode(x.id).starts_with(pattern));
|
|
||||||
if candidates.is_empty() {
|
|
||||||
Err(Error::NoSuchBucket(pattern.clone()))
|
|
||||||
} else if candidates.len() == 1 {
|
|
||||||
Ok(candidates.into_iter().next().unwrap().id)
|
|
||||||
} else {
|
|
||||||
Err(Error::BadRequest(format!(
|
|
||||||
"Several matching buckets: {}",
|
|
||||||
pattern
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a Bucket if it is present in bucket table,
|
/// Returns a Bucket if it is present in bucket table,
|
||||||
/// even if it is in deleted state. Querying a non-existing
|
/// even if it is in deleted state. Querying a non-existing
|
||||||
/// bucket ID returns an internal error.
|
/// bucket ID returns an internal error.
|
||||||
|
|
|
@ -3,7 +3,7 @@ use garage_util::error::OkOrMessage;
|
||||||
|
|
||||||
use crate::garage::Garage;
|
use crate::garage::Garage;
|
||||||
use crate::helper::error::*;
|
use crate::helper::error::*;
|
||||||
use crate::key_table::{Key, KeyFilter};
|
use crate::key_table::Key;
|
||||||
|
|
||||||
pub struct KeyHelper<'a>(pub(crate) &'a Garage);
|
pub struct KeyHelper<'a>(pub(crate) &'a Garage);
|
||||||
|
|
||||||
|
@ -33,33 +33,4 @@ impl<'a> KeyHelper<'a> {
|
||||||
.filter(|b| !b.state.is_deleted())
|
.filter(|b| !b.state.is_deleted())
|
||||||
.ok_or_else(|| Error::NoSuchAccessKey(key_id.to_string()))
|
.ok_or_else(|| Error::NoSuchAccessKey(key_id.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a Key if it is present in key table,
|
|
||||||
/// looking it up by key ID or by a match on its name,
|
|
||||||
/// only if it is in non-deleted state.
|
|
||||||
/// Querying a non-existing key ID or a deleted key
|
|
||||||
/// returns a bad request error.
|
|
||||||
pub async fn get_existing_matching_key(&self, pattern: &str) -> Result<Key, Error> {
|
|
||||||
let candidates = self
|
|
||||||
.0
|
|
||||||
.key_table
|
|
||||||
.get_range(
|
|
||||||
&EmptyKey,
|
|
||||||
None,
|
|
||||||
Some(KeyFilter::MatchesAndNotDeleted(pattern.to_string())),
|
|
||||||
10,
|
|
||||||
EnumerationOrder::Forward,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if candidates.len() != 1 {
|
|
||||||
Err(Error::BadRequest(format!(
|
|
||||||
"{} matching keys",
|
|
||||||
candidates.len()
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(candidates.into_iter().next().unwrap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub mod permission;
|
||||||
|
|
||||||
pub mod index_counter;
|
pub mod index_counter;
|
||||||
|
|
||||||
|
pub mod admin_token_table;
|
||||||
pub mod bucket_alias_table;
|
pub mod bucket_alias_table;
|
||||||
pub mod bucket_table;
|
pub mod bucket_table;
|
||||||
pub mod key_table;
|
pub mod key_table;
|
||||||
|
|
|
@ -198,6 +198,9 @@ pub struct AdminConfig {
|
||||||
pub metrics_token: Option<String>,
|
pub metrics_token: Option<String>,
|
||||||
/// File to read metrics token from
|
/// File to read metrics token from
|
||||||
pub metrics_token_file: Option<PathBuf>,
|
pub metrics_token_file: Option<PathBuf>,
|
||||||
|
/// Whether to require an access token for accessing the metrics endpoint
|
||||||
|
#[serde(default)]
|
||||||
|
pub metrics_require_token: bool,
|
||||||
|
|
||||||
/// Bearer token to use to access Admin API endpoints
|
/// Bearer token to use to access Admin API endpoints
|
||||||
pub admin_token: Option<String>,
|
pub admin_token: Option<String>,
|
||||||
|
|
Loading…
Add table
Reference in a new issue