mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-08-24 20:38:34 +00:00
feat: add min-tls option to provider config (#931)
* feat: add min-tls option to provider config Signed-off-by: Robert Kaussow <mail@thegeeklab.de> * fix: clenup code and add min-tls to file resource Signed-off-by: Robert Kaussow <mail@thegeeklab.de> * fix: linter errors, wrong schema type in file resource Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> * chore: update docs Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: Robert Kaussow <mail@thegeeklab.de> Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
7d94bf73ec
commit
01ff2cb7db
@ -21,7 +21,7 @@ provider "proxmox" {
|
|||||||
password = "the-password-set-during-installation-of-proxmox-ve"
|
password = "the-password-set-during-installation-of-proxmox-ve"
|
||||||
# because self-signed TLS certificate is in use
|
# because self-signed TLS certificate is in use
|
||||||
insecure = true
|
insecure = true
|
||||||
# uncoment (unless on Windows...)
|
# uncomment (unless on Windows...)
|
||||||
# tmp_dir = "/var/tmp"
|
# tmp_dir = "/var/tmp"
|
||||||
|
|
||||||
ssh {
|
ssh {
|
||||||
@ -222,6 +222,7 @@ In addition to [generic provider arguments](https://www.terraform.io/docs/config
|
|||||||
|
|
||||||
- `endpoint` - (Required) The endpoint for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_ENDPOINT`). Usually this is `https://<your-cluster-endpoint>:8006/`. **Do not** include `/api2/json` at the end.
|
- `endpoint` - (Required) The endpoint for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_ENDPOINT`). Usually this is `https://<your-cluster-endpoint>:8006/`. **Do not** include `/api2/json` at the end.
|
||||||
- `insecure` - (Optional) Whether to skip the TLS verification step (can also be sourced from `PROXMOX_VE_INSECURE`). If omitted, defaults to `false`.
|
- `insecure` - (Optional) Whether to skip the TLS verification step (can also be sourced from `PROXMOX_VE_INSECURE`). If omitted, defaults to `false`.
|
||||||
|
- `min_tls` - (Optional) The minimum required TLS version for API calls (can also be sourced from `PROXMOX_VE_MIN_TLS`). Supported values: `1.0|1.1|1.2|1.3`. If omitted, defaults to `1.3`.
|
||||||
- `otp` - (Optional, Deprecated) The one-time password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_OTP`).
|
- `otp` - (Optional, Deprecated) The one-time password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_OTP`).
|
||||||
- `password` - (Required) The password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_PASSWORD`).
|
- `password` - (Required) The password for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_PASSWORD`).
|
||||||
- `username` - (Required) The username and realm for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_USERNAME`). For example, `root@pam`.
|
- `username` - (Required) The username and realm for the Proxmox Virtual Environment API (can also be sourced from `PROXMOX_VE_USERNAME`). For example, `root@pam`.
|
||||||
|
@ -117,6 +117,7 @@ resource "proxmox_virtual_environment_file" "ubuntu_container_template" {
|
|||||||
when the source file is a URL referencing a `.qcow2` image.
|
when the source file is a URL referencing a `.qcow2` image.
|
||||||
- `insecure` - (Optional) Whether to skip the TLS verification step for
|
- `insecure` - (Optional) Whether to skip the TLS verification step for
|
||||||
HTTPS sources (defaults to `false`).
|
HTTPS sources (defaults to `false`).
|
||||||
|
- `min_tls` - (Optional) The minimum required TLS version for HTTPS sources. "Supported values: `1.0|1.1|1.2|1.3` (defaults to `1.3`).
|
||||||
- `path` - (Required) A path to a local file or a URL.
|
- `path` - (Required) A path to a local file or a URL.
|
||||||
- `source_raw` - (Optional) The raw source (conflicts with `source_file`).
|
- `source_raw` - (Optional) The raw source (conflicts with `source_file`).
|
||||||
- `data` - (Required) The raw data.
|
- `data` - (Required) The raw data.
|
||||||
|
@ -56,6 +56,7 @@ type proxmoxProviderModel struct {
|
|||||||
APIToken types.String `tfsdk:"api_token"`
|
APIToken types.String `tfsdk:"api_token"`
|
||||||
Endpoint types.String `tfsdk:"endpoint"`
|
Endpoint types.String `tfsdk:"endpoint"`
|
||||||
Insecure types.Bool `tfsdk:"insecure"`
|
Insecure types.Bool `tfsdk:"insecure"`
|
||||||
|
MinTLS types.String `tfsdk:"min_tls"`
|
||||||
OTP types.String `tfsdk:"otp"`
|
OTP types.String `tfsdk:"otp"`
|
||||||
Username types.String `tfsdk:"username"`
|
Username types.String `tfsdk:"username"`
|
||||||
Password types.String `tfsdk:"password"`
|
Password types.String `tfsdk:"password"`
|
||||||
@ -106,6 +107,11 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
|
|||||||
Description: "Whether to skip the TLS verification step.",
|
Description: "Whether to skip the TLS verification step.",
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
"min_tls": schema.StringAttribute{
|
||||||
|
Description: "The minimum required TLS version for API calls." +
|
||||||
|
"Supported values: `1.0|1.1|1.2|1.3`. Defaults to `1.3`.",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
"otp": schema.StringAttribute{
|
"otp": schema.StringAttribute{
|
||||||
Description: "The one-time password for the Proxmox VE API.",
|
Description: "The one-time password for the Proxmox VE API.",
|
||||||
Optional: true,
|
Optional: true,
|
||||||
@ -228,6 +234,7 @@ func (p *proxmoxProvider) Configure(
|
|||||||
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
|
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
|
||||||
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT")
|
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT")
|
||||||
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE")
|
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE")
|
||||||
|
minTLS := utils.GetAnyStringEnv("PROXMOX_VE_MIN_TLS")
|
||||||
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME")
|
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME")
|
||||||
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD")
|
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD")
|
||||||
|
|
||||||
@ -243,6 +250,10 @@ func (p *proxmoxProvider) Configure(
|
|||||||
insecure = config.Insecure.ValueBool()
|
insecure = config.Insecure.ValueBool()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !config.MinTLS.IsNull() {
|
||||||
|
minTLS = config.MinTLS.ValueString()
|
||||||
|
}
|
||||||
|
|
||||||
if !config.Username.IsNull() {
|
if !config.Username.IsNull() {
|
||||||
username = config.Username.ValueString()
|
username = config.Username.ValueString()
|
||||||
}
|
}
|
||||||
@ -278,6 +289,7 @@ func (p *proxmoxProvider) Configure(
|
|||||||
conn, err := api.NewConnection(
|
conn, err := api.NewConnection(
|
||||||
endpoint,
|
endpoint,
|
||||||
insecure,
|
insecure,
|
||||||
|
minTLS,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError(
|
resp.Diagnostics.AddError(
|
||||||
|
@ -92,7 +92,7 @@ func getNodesClient() *nodes.Client {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := api.NewConnection(endpoint, true)
|
conn, err := api.NewConnection(endpoint, true, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/bpg/terraform-provider-proxmox/utils"
|
"github.com/bpg/terraform-provider-proxmox/utils"
|
||||||
)
|
)
|
||||||
@ -59,7 +61,7 @@ type Connection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewConnection creates and initializes a Connection instance.
|
// NewConnection creates and initializes a Connection instance.
|
||||||
func NewConnection(endpoint string, insecure bool) (*Connection, error) {
|
func NewConnection(endpoint string, insecure bool, minTLS string) (*Connection, error) {
|
||||||
u, err := url.ParseRequestURI(endpoint)
|
u, err := url.ParseRequestURI(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(
|
return nil, errors.New(
|
||||||
@ -73,10 +75,15 @@ func NewConnection(endpoint string, insecure bool) (*Connection, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
version, err := GetMinTLSVersion(minTLS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var transport http.RoundTripper = &http.Transport{
|
var transport http.RoundTripper = &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
MinVersion: tls.VersionTLS13,
|
MinVersion: version,
|
||||||
InsecureSkipVerify: insecure, //nolint:gosec
|
InsecureSkipVerify: insecure, //nolint:gosec
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -317,3 +324,24 @@ func validateResponseCode(res *http.Response) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMinTLSVersion returns the minimum TLS version constant for the given string. If the string is empty,
|
||||||
|
// the default TLS version is returned. For unsupported TLS versions, an error is returned.
|
||||||
|
func GetMinTLSVersion(version string) (uint16, error) {
|
||||||
|
validVersions := map[string]uint16{
|
||||||
|
"": tls.VersionTLS13,
|
||||||
|
"1.3": tls.VersionTLS13,
|
||||||
|
"1.2": tls.VersionTLS12,
|
||||||
|
"1.1": tls.VersionTLS11,
|
||||||
|
"1.0": tls.VersionTLS10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := validVersions[strings.TrimSpace(version)]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
valid := maps.Keys(validVersions)
|
||||||
|
sort.Strings(valid)
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("unsupported minimal TLS version %s, must be one of: %s", version, strings.Join(valid, ", "))
|
||||||
|
}
|
||||||
|
@ -50,6 +50,7 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
|
|||||||
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN", "PM_VE_API_TOKEN")
|
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN", "PM_VE_API_TOKEN")
|
||||||
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT", "PM_VE_ENDPOINT")
|
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT", "PM_VE_ENDPOINT")
|
||||||
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE", "PM_VE_INSECURE")
|
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE", "PM_VE_INSECURE")
|
||||||
|
minTLS := utils.GetAnyStringEnv("PROXMOX_VE_MIN_TLS", "PM_VE_MIN_TLS")
|
||||||
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME", "PM_VE_USERNAME")
|
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME", "PM_VE_USERNAME")
|
||||||
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD", "PM_VE_PASSWORD")
|
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD", "PM_VE_PASSWORD")
|
||||||
otp := utils.GetAnyStringEnv("PROXMOX_VE_OTP", "PM_VE_OTP")
|
otp := utils.GetAnyStringEnv("PROXMOX_VE_OTP", "PM_VE_OTP")
|
||||||
@ -66,6 +67,10 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
|
|||||||
insecure = v.(bool)
|
insecure = v.(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, ok := d.GetOk(mkProviderMinTLS); ok {
|
||||||
|
minTLS = v.(string)
|
||||||
|
}
|
||||||
|
|
||||||
if v, ok := d.GetOk(mkProviderUsername); ok {
|
if v, ok := d.GetOk(mkProviderUsername); ok {
|
||||||
username = v.(string)
|
username = v.(string)
|
||||||
}
|
}
|
||||||
@ -81,7 +86,7 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
|
|||||||
creds, err = api.NewCredentials(username, password, otp, apiToken)
|
creds, err = api.NewCredentials(username, password, otp, apiToken)
|
||||||
diags = append(diags, diag.FromErr(err)...)
|
diags = append(diags, diag.FromErr(err)...)
|
||||||
|
|
||||||
conn, err = api.NewConnection(endpoint, insecure)
|
conn, err = api.NewConnection(endpoint, insecure, minTLS)
|
||||||
diags = append(diags, diag.FromErr(err)...)
|
diags = append(diags, diag.FromErr(err)...)
|
||||||
|
|
||||||
if diags.HasError() {
|
if diags.HasError() {
|
||||||
|
@ -37,6 +37,7 @@ func TestProviderSchema(t *testing.T) {
|
|||||||
mkProviderPassword,
|
mkProviderPassword,
|
||||||
mkProviderEndpoint,
|
mkProviderEndpoint,
|
||||||
mkProviderInsecure,
|
mkProviderInsecure,
|
||||||
|
mkProviderMinTLS,
|
||||||
mkProviderOTP,
|
mkProviderOTP,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ func TestProviderSchema(t *testing.T) {
|
|||||||
mkProviderPassword: schema.TypeString,
|
mkProviderPassword: schema.TypeString,
|
||||||
mkProviderEndpoint: schema.TypeString,
|
mkProviderEndpoint: schema.TypeString,
|
||||||
mkProviderInsecure: schema.TypeBool,
|
mkProviderInsecure: schema.TypeBool,
|
||||||
|
mkProviderMinTLS: schema.TypeString,
|
||||||
mkProviderOTP: schema.TypeString,
|
mkProviderOTP: schema.TypeString,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ const (
|
|||||||
dvProviderOTP = ""
|
dvProviderOTP = ""
|
||||||
mkProviderEndpoint = "endpoint"
|
mkProviderEndpoint = "endpoint"
|
||||||
mkProviderInsecure = "insecure"
|
mkProviderInsecure = "insecure"
|
||||||
|
mkProviderMinTLS = "min_tls"
|
||||||
mkProviderOTP = "otp"
|
mkProviderOTP = "otp"
|
||||||
mkProviderPassword = "password"
|
mkProviderPassword = "password"
|
||||||
mkProviderUsername = "username"
|
mkProviderUsername = "username"
|
||||||
@ -48,6 +49,12 @@ func createSchema() map[string]*schema.Schema {
|
|||||||
Optional: true,
|
Optional: true,
|
||||||
Description: "Whether to skip the TLS verification step.",
|
Description: "Whether to skip the TLS verification step.",
|
||||||
},
|
},
|
||||||
|
mkProviderMinTLS: {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Description: "The minimum required TLS version for API calls." +
|
||||||
|
"Supported values: `1.0|1.1|1.2|1.3`. Defaults to `1.3`.",
|
||||||
|
},
|
||||||
mkProviderOTP: {
|
mkProviderOTP: {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
@ -39,6 +39,7 @@ const (
|
|||||||
dvResourceVirtualEnvironmentFileSourceFileChecksum = ""
|
dvResourceVirtualEnvironmentFileSourceFileChecksum = ""
|
||||||
dvResourceVirtualEnvironmentFileSourceFileFileName = ""
|
dvResourceVirtualEnvironmentFileSourceFileFileName = ""
|
||||||
dvResourceVirtualEnvironmentFileSourceFileInsecure = false
|
dvResourceVirtualEnvironmentFileSourceFileInsecure = false
|
||||||
|
dvResourceVirtualEnvironmentFileSourceFileMinTLS = ""
|
||||||
dvResourceVirtualEnvironmentFileOverwrite = true
|
dvResourceVirtualEnvironmentFileOverwrite = true
|
||||||
dvResourceVirtualEnvironmentFileSourceRawResize = 0
|
dvResourceVirtualEnvironmentFileSourceRawResize = 0
|
||||||
dvResourceVirtualEnvironmentFileTimeoutUpload = 1800
|
dvResourceVirtualEnvironmentFileTimeoutUpload = 1800
|
||||||
@ -57,6 +58,7 @@ const (
|
|||||||
mkResourceVirtualEnvironmentFileSourceFileChecksum = "checksum"
|
mkResourceVirtualEnvironmentFileSourceFileChecksum = "checksum"
|
||||||
mkResourceVirtualEnvironmentFileSourceFileFileName = "file_name"
|
mkResourceVirtualEnvironmentFileSourceFileFileName = "file_name"
|
||||||
mkResourceVirtualEnvironmentFileSourceFileInsecure = "insecure"
|
mkResourceVirtualEnvironmentFileSourceFileInsecure = "insecure"
|
||||||
|
mkResourceVirtualEnvironmentFileSourceFileMinTLS = "min_tls"
|
||||||
mkResourceVirtualEnvironmentFileSourceRaw = "source_raw"
|
mkResourceVirtualEnvironmentFileSourceRaw = "source_raw"
|
||||||
mkResourceVirtualEnvironmentFileSourceRawData = "data"
|
mkResourceVirtualEnvironmentFileSourceRawData = "data"
|
||||||
mkResourceVirtualEnvironmentFileSourceRawFileName = "file_name"
|
mkResourceVirtualEnvironmentFileSourceRawFileName = "file_name"
|
||||||
@ -155,6 +157,14 @@ func File() *schema.Resource {
|
|||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
Default: dvResourceVirtualEnvironmentFileSourceFileInsecure,
|
Default: dvResourceVirtualEnvironmentFileSourceFileInsecure,
|
||||||
},
|
},
|
||||||
|
mkResourceVirtualEnvironmentFileSourceFileMinTLS: {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Description: "The minimum required TLS version for HTTPS sources." +
|
||||||
|
"Supported values: `1.0|1.1|1.2|1.3`. Defaults to `1.3`.",
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Default: dvResourceVirtualEnvironmentFileSourceFileMinTLS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MaxItems: 1,
|
MaxItems: 1,
|
||||||
@ -372,6 +382,7 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag
|
|||||||
sourceFileBlock := sourceFile[0].(map[string]interface{})
|
sourceFileBlock := sourceFile[0].(map[string]interface{})
|
||||||
sourceFilePath := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
|
sourceFilePath := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
|
||||||
sourceFileChecksum := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileChecksum].(string)
|
sourceFileChecksum := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileChecksum].(string)
|
||||||
|
sourceFileMinTLS := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileMinTLS].(string)
|
||||||
sourceFileInsecure := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileInsecure].(bool)
|
sourceFileInsecure := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileInsecure].(bool)
|
||||||
|
|
||||||
if fileIsURL(d) {
|
if fileIsURL(d) {
|
||||||
@ -379,10 +390,15 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag
|
|||||||
"url": sourceFilePath,
|
"url": sourceFilePath,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
version, e := api.GetMinTLSVersion(sourceFileMinTLS)
|
||||||
|
if e != nil {
|
||||||
|
return diag.FromErr(e)
|
||||||
|
}
|
||||||
|
|
||||||
httpClient := http.Client{
|
httpClient := http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
MinVersion: tls.VersionTLS13,
|
MinVersion: version,
|
||||||
InsecureSkipVerify: sourceFileInsecure,
|
InsecureSkipVerify: sourceFileInsecure,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user