mirror of
https://github.com/status-im/consul.git
synced 2025-01-10 13:55:55 +00:00
Refactor config checks oss (#12550)
Currently the config_entry.go subsystem delegates authorization decisions via the ConfigEntry interface CanRead and CanWrite code. Unfortunately this returns a true/false value and loses the details of the source. This is not helpful, especially since it the config subsystem can be more complex to understand, since it covers so many domains. This refactors CanRead/CanWrite to return a structured error message (PermissionDenied or the like) with more details about the reason for denial. Part of #12241 Signed-off-by: Mark Anderson <manderson@hashicorp.com>
This commit is contained in:
parent
b9a5e10aea
commit
676ea58bc4
@ -89,8 +89,8 @@ func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !args.Entry.CanWrite(authz) {
|
if err := args.Entry.CanWrite(authz); err != nil {
|
||||||
return acl.ErrPermissionDenied // TODO(acl-error-enhancements) Better errors await refactoring of CanWrite above.
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.Op != structs.ConfigEntryUpsert && args.Op != structs.ConfigEntryUpsertCAS {
|
if args.Op != structs.ConfigEntryUpsert && args.Op != structs.ConfigEntryUpsertCAS {
|
||||||
@ -194,8 +194,8 @@ func (c *ConfigEntry) Get(args *structs.ConfigEntryQuery, reply *structs.ConfigE
|
|||||||
}
|
}
|
||||||
lookupEntry.GetEnterpriseMeta().Merge(&args.EnterpriseMeta)
|
lookupEntry.GetEnterpriseMeta().Merge(&args.EnterpriseMeta)
|
||||||
|
|
||||||
if !lookupEntry.CanRead(authz) {
|
if err := lookupEntry.CanRead(authz); err != nil {
|
||||||
return acl.ErrPermissionDenied
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.srv.blockingQuery(
|
return c.srv.blockingQuery(
|
||||||
@ -254,7 +254,8 @@ func (c *ConfigEntry) List(args *structs.ConfigEntryQuery, reply *structs.Indexe
|
|||||||
// Filter the entries returned by ACL permissions.
|
// Filter the entries returned by ACL permissions.
|
||||||
filteredEntries := make([]structs.ConfigEntry, 0, len(entries))
|
filteredEntries := make([]structs.ConfigEntry, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !entry.CanRead(authz) {
|
if err := entry.CanRead(authz); err != nil {
|
||||||
|
// TODO we may wish to extract more details from this error to aid user comprehension
|
||||||
reply.QueryMeta.ResultsFilteredByACLs = true
|
reply.QueryMeta.ResultsFilteredByACLs = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -335,7 +336,8 @@ func (c *ConfigEntry) ListAll(args *structs.ConfigEntryListAllRequest, reply *st
|
|||||||
// Filter the entries returned by ACL permissions or by the provided kinds.
|
// Filter the entries returned by ACL permissions or by the provided kinds.
|
||||||
filteredEntries := make([]structs.ConfigEntry, 0, len(entries))
|
filteredEntries := make([]structs.ConfigEntry, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !entry.CanRead(authz) {
|
if err := entry.CanRead(authz); err != nil {
|
||||||
|
// TODO we may wish to extract more details from this error to aid user comprehension
|
||||||
reply.QueryMeta.ResultsFilteredByACLs = true
|
reply.QueryMeta.ResultsFilteredByACLs = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -386,8 +388,8 @@ func (c *ConfigEntry) Delete(args *structs.ConfigEntryRequest, reply *structs.Co
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !args.Entry.CanWrite(authz) {
|
if err := args.Entry.CanWrite(authz); err != nil {
|
||||||
return acl.ErrPermissionDenied
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only delete and delete-cas ops are supported. If the caller erroneously
|
// Only delete and delete-cas ops are supported. If the caller erroneously
|
||||||
|
@ -273,7 +273,7 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
|
|||||||
// If the token provided does not have the necessary permissions,
|
// If the token provided does not have the necessary permissions,
|
||||||
// write a forbidden response
|
// write a forbidden response
|
||||||
// TODO(partitions): should this be possible in a partition?
|
// TODO(partitions): should this be possible in a partition?
|
||||||
// TODO((acl-error-enhancements)): We should return error details somehow here.
|
// TODO(acl-error-enhancements): We should return error details somehow here.
|
||||||
if authz.OperatorRead(nil) != acl.Allow {
|
if authz.OperatorRead(nil) != acl.Allow {
|
||||||
resp.WriteHeader(http.StatusForbidden)
|
resp.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
@ -60,8 +60,8 @@ type ConfigEntry interface {
|
|||||||
|
|
||||||
// CanRead and CanWrite return whether or not the given Authorizer
|
// CanRead and CanWrite return whether or not the given Authorizer
|
||||||
// has permission to read or write to the config entry, respectively.
|
// has permission to read or write to the config entry, respectively.
|
||||||
CanRead(acl.Authorizer) bool
|
CanRead(acl.Authorizer) error
|
||||||
CanWrite(acl.Authorizer) bool
|
CanWrite(acl.Authorizer) error
|
||||||
|
|
||||||
GetMeta() map[string]string
|
GetMeta() map[string]string
|
||||||
GetEnterpriseMeta() *EnterpriseMeta
|
GetEnterpriseMeta() *EnterpriseMeta
|
||||||
@ -183,16 +183,16 @@ func (e *ServiceConfigEntry) Validate() error {
|
|||||||
return validationErr
|
return validationErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *ServiceConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.ServiceRead(e.Name, &authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(e.Name, &authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *ServiceConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.ServiceWrite(e.Name, &authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().ServiceWriteAllowed(e.Name, &authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceConfigEntry) GetRaftIndex() *RaftIndex {
|
func (e *ServiceConfigEntry) GetRaftIndex() *RaftIndex {
|
||||||
@ -306,14 +306,14 @@ func (e *ProxyConfigEntry) Validate() error {
|
|||||||
return e.validateEnterpriseMeta()
|
return e.validateEnterpriseMeta()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ProxyConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *ProxyConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ProxyConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *ProxyConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.MeshWrite(&authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ProxyConfigEntry) GetRaftIndex() *RaftIndex {
|
func (e *ProxyConfigEntry) GetRaftIndex() *RaftIndex {
|
||||||
|
@ -251,11 +251,11 @@ func isValidHTTPMethod(method string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceRouterConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *ServiceRouterConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
return canReadDiscoveryChain(e, authz)
|
return canReadDiscoveryChain(e, authz)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceRouterConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *ServiceRouterConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
return canWriteDiscoveryChain(e, authz)
|
return canWriteDiscoveryChain(e, authz)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -594,11 +594,11 @@ func scaleWeight(v float32) int {
|
|||||||
return int(math.Round(float64(v * 100.0)))
|
return int(math.Round(float64(v * 100.0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceSplitterConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *ServiceSplitterConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
return canReadDiscoveryChain(e, authz)
|
return canReadDiscoveryChain(e, authz)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceSplitterConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *ServiceSplitterConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
return canWriteDiscoveryChain(e, authz)
|
return canWriteDiscoveryChain(e, authz)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1069,11 +1069,11 @@ func (e *ServiceResolverConfigEntry) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceResolverConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *ServiceResolverConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
return canReadDiscoveryChain(e, authz)
|
return canReadDiscoveryChain(e, authz)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceResolverConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *ServiceResolverConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
return canWriteDiscoveryChain(e, authz)
|
return canWriteDiscoveryChain(e, authz)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1300,13 +1300,13 @@ type discoveryChainConfigEntry interface {
|
|||||||
ListRelatedServices() []ServiceID
|
ListRelatedServices() []ServiceID
|
||||||
}
|
}
|
||||||
|
|
||||||
func canReadDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorizer) bool {
|
func canReadDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
entry.GetEnterpriseMeta().FillAuthzContext(&authzContext)
|
entry.GetEnterpriseMeta().FillAuthzContext(&authzContext)
|
||||||
return authz.ServiceRead(entry.GetName(), &authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(entry.GetName(), &authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func canWriteDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorizer) bool {
|
func canWriteDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorizer) error {
|
||||||
entryID := NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())
|
entryID := NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())
|
||||||
|
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
@ -1314,8 +1314,8 @@ func canWriteDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorize
|
|||||||
|
|
||||||
name := entry.GetName()
|
name := entry.GetName()
|
||||||
|
|
||||||
if authz.ServiceWrite(name, &authzContext) != acl.Allow {
|
if err := authz.ToAllowAuthorizer().ServiceWriteAllowed(name, &authzContext); err != nil {
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, svc := range entry.ListRelatedServices() {
|
for _, svc := range entry.ListRelatedServices() {
|
||||||
@ -1326,11 +1326,11 @@ func canWriteDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorize
|
|||||||
svc.FillAuthzContext(&authzContext)
|
svc.FillAuthzContext(&authzContext)
|
||||||
// You only need read on related services to redirect traffic flow for
|
// You only need read on related services to redirect traffic flow for
|
||||||
// your own service.
|
// your own service.
|
||||||
if authz.ServiceRead(svc.ID, &authzContext) != acl.Allow {
|
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(svc.ID, &authzContext); err != nil {
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiscoveryChainRequest is used when requesting the discovery chain for a
|
// DiscoveryChainRequest is used when requesting the discovery chain for a
|
||||||
|
@ -136,16 +136,16 @@ func (e *ExportedServicesConfigEntry) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExportedServicesConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *ExportedServicesConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.MeshRead(&authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().MeshReadAllowed(&authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExportedServicesConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *ExportedServicesConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.MeshWrite(&authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExportedServicesConfigEntry) GetRaftIndex() *RaftIndex {
|
func (e *ExportedServicesConfigEntry) GetRaftIndex() *RaftIndex {
|
||||||
|
@ -430,16 +430,16 @@ func (e *IngressGatewayConfigEntry) ListRelatedServices() []ServiceID {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.ServiceRead(e.Name, &authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(e.Name, &authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.MeshWrite(&authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *IngressGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
func (e *IngressGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
||||||
@ -572,16 +572,16 @@ func (e *TerminatingGatewayConfigEntry) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *TerminatingGatewayConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *TerminatingGatewayConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.ServiceRead(e.Name, &authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(e.Name, &authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *TerminatingGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *TerminatingGatewayConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.MeshWrite(&authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *TerminatingGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
func (e *TerminatingGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
||||||
|
@ -789,16 +789,16 @@ func (e *ServiceIntentionsConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
|
|||||||
return &e.EnterpriseMeta
|
return &e.EnterpriseMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceIntentionsConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *ServiceIntentionsConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.IntentionRead(e.GetName(), &authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().IntentionReadAllowed(e.GetName(), &authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServiceIntentionsConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *ServiceIntentionsConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.IntentionWrite(e.GetName(), &authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().IntentionWriteAllowed(e.GetName(), &authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry {
|
func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry {
|
||||||
|
@ -64,14 +64,14 @@ func (e *MeshConfigEntry) Validate() error {
|
|||||||
return e.validateEnterpriseMeta()
|
return e.validateEnterpriseMeta()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *MeshConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *MeshConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *MeshConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *MeshConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.MeshWrite(&authzContext) == acl.Allow
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *MeshConfigEntry) GetRaftIndex() *RaftIndex {
|
func (e *MeshConfigEntry) GetRaftIndex() *RaftIndex {
|
||||||
|
@ -194,8 +194,20 @@ func testConfigEntries_ListRelatedServices_AndACLs(t *testing.T, cases []configE
|
|||||||
for _, a := range tc.expectACLs {
|
for _, a := range tc.expectACLs {
|
||||||
require.NotEmpty(t, a.name)
|
require.NotEmpty(t, a.name)
|
||||||
t.Run(a.name, func(t *testing.T) {
|
t.Run(a.name, func(t *testing.T) {
|
||||||
require.Equal(t, a.canRead, tc.entry.CanRead(a.authorizer), "unexpected CanRead result")
|
canRead := tc.entry.CanRead(a.authorizer)
|
||||||
require.Equal(t, a.canWrite, tc.entry.CanWrite(a.authorizer), "unexpected CanWrite result")
|
if a.canRead {
|
||||||
|
require.Nil(t, canRead)
|
||||||
|
} else {
|
||||||
|
require.Error(t, canRead)
|
||||||
|
require.True(t, acl.IsErrPermissionDenied(canRead))
|
||||||
|
}
|
||||||
|
canWrite := tc.entry.CanWrite(a.authorizer)
|
||||||
|
if a.canWrite {
|
||||||
|
require.Nil(t, canWrite)
|
||||||
|
} else {
|
||||||
|
require.Error(t, canWrite)
|
||||||
|
require.True(t, acl.IsErrPermissionDenied(canWrite))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user