mirror of
https://github.com/overte-org/overte.git
synced 2025-08-10 19:03:07 +02:00
clean domain metadata and update acl
This commit is contained in:
parent
40f2d36487
commit
25b21dacda
4 changed files with 154 additions and 100 deletions
|
@ -10,16 +10,18 @@
|
||||||
|
|
||||||
#include "DomainMetadata.h"
|
#include "DomainMetadata.h"
|
||||||
|
|
||||||
#include <HifiConfigVariantMap.h>
|
#include <AccountManager.h>
|
||||||
#include <DependencyManager.h>
|
#include <DependencyManager.h>
|
||||||
|
#include <HifiConfigVariantMap.h>
|
||||||
#include <LimitedNodeList.h>
|
#include <LimitedNodeList.h>
|
||||||
|
|
||||||
|
#include "DomainServer.h"
|
||||||
#include "DomainServerNodeData.h"
|
#include "DomainServerNodeData.h"
|
||||||
|
|
||||||
const QString DomainMetadata::USERS = "users";
|
const QString DomainMetadata::USERS = "users";
|
||||||
const QString DomainMetadata::USERS_NUM_TOTAL = "num_users";
|
const QString DomainMetadata::Users::NUM_TOTAL = "num_users";
|
||||||
const QString DomainMetadata::USERS_NUM_ANON = "num_anon_users";
|
const QString DomainMetadata::Users::NUM_ANON = "num_anon_users";
|
||||||
const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames";
|
const QString DomainMetadata::Users::HOSTNAMES = "user_hostnames";
|
||||||
// users metadata will appear as (JSON):
|
// users metadata will appear as (JSON):
|
||||||
// { "num_users": Number,
|
// { "num_users": Number,
|
||||||
// "num_anon_users": Number,
|
// "num_anon_users": Number,
|
||||||
|
@ -27,25 +29,20 @@ const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames";
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const QString DomainMetadata::DESCRIPTORS = "descriptors";
|
const QString DomainMetadata::DESCRIPTORS = "descriptors";
|
||||||
const QString DomainMetadata::DESCRIPTORS_DESCRIPTION = "description";
|
const QString DomainMetadata::Descriptors::DESCRIPTION = "description";
|
||||||
const QString DomainMetadata::DESCRIPTORS_CAPACITY = "capacity"; // parsed from security
|
const QString DomainMetadata::Descriptors::CAPACITY = "capacity"; // parsed from security
|
||||||
const QString DomainMetadata::DESCRIPTORS_RESTRICTION = "restriction"; // parsed from ACL
|
const QString DomainMetadata::Descriptors::HOURS = "hours";
|
||||||
const QString DomainMetadata::DESCRIPTORS_MATURITY = "maturity";
|
const QString DomainMetadata::Descriptors::RESTRICTION = "restriction"; // parsed from ACL
|
||||||
const QString DomainMetadata::DESCRIPTORS_HOSTS = "hosts";
|
const QString DomainMetadata::Descriptors::MATURITY = "maturity";
|
||||||
const QString DomainMetadata::DESCRIPTORS_TAGS = "tags";
|
const QString DomainMetadata::Descriptors::HOSTS = "hosts";
|
||||||
|
const QString DomainMetadata::Descriptors::TAGS = "tags";
|
||||||
// descriptors metadata will appear as (JSON):
|
// descriptors metadata will appear as (JSON):
|
||||||
// { "capacity": Number,
|
// { "description": String, // capped description
|
||||||
// TODO: "hours": String, // UTF-8 representation of the week, split into 15" segments
|
// "capacity": Number,
|
||||||
|
// "hours": String, // UTF-8 representation of the week, split into 15" segments
|
||||||
// "restriction": String, // enum of either open, hifi, or acl
|
// "restriction": String, // enum of either open, hifi, or acl
|
||||||
// "maturity": String, // enum corresponding to ESRB ratings
|
// "maturity": String, // enum corresponding to ESRB ratings
|
||||||
// "hosts": [ String ], // capped list of usernames
|
// "hosts": [ String ], // capped list of usernames
|
||||||
// "description": String, // capped description
|
|
||||||
// TODO: "img": {
|
|
||||||
// "src": String,
|
|
||||||
// "type": String,
|
|
||||||
// "size": Number,
|
|
||||||
// "updated_at": Number,
|
|
||||||
// },
|
|
||||||
// "tags": [ String ], // capped list of tags
|
// "tags": [ String ], // capped list of tags
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
@ -54,36 +51,103 @@ const QString DomainMetadata::DESCRIPTORS_TAGS = "tags";
|
||||||
//
|
//
|
||||||
// it is meant to be sent to and consumed by an external API
|
// it is meant to be sent to and consumed by an external API
|
||||||
|
|
||||||
DomainMetadata::DomainMetadata() {
|
DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) {
|
||||||
_metadata[USERS] = {};
|
_metadata[USERS] = {};
|
||||||
_metadata[DESCRIPTORS] = {};
|
_metadata[DESCRIPTORS] = {};
|
||||||
|
|
||||||
|
assert(dynamic_cast<DomainServer*>(domainServer));
|
||||||
|
DomainServer* server = static_cast<DomainServer*>(domainServer);
|
||||||
|
|
||||||
|
// update the metadata when a user (dis)connects
|
||||||
|
connect(server, &DomainServer::userConnected, this, &DomainMetadata::usersChanged);
|
||||||
|
connect(server, &DomainServer::userDisconnected, this, &DomainMetadata::usersChanged);
|
||||||
|
|
||||||
|
// update the metadata when security changes
|
||||||
|
connect(&server->_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
|
||||||
|
this, static_cast<void(DomainMetadata::*)()>(&DomainMetadata::securityChanged));
|
||||||
|
|
||||||
|
// initialize the descriptors
|
||||||
|
descriptorsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DomainMetadata::setDescriptors(QVariantMap& settings) {
|
QJsonObject DomainMetadata::get() {
|
||||||
|
maybeUpdateUsers();
|
||||||
|
return QJsonObject::fromVariantMap(_metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject DomainMetadata::get(const QString& group) {
|
||||||
|
maybeUpdateUsers();
|
||||||
|
return QJsonObject::fromVariantMap(_metadata[group].toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainMetadata::descriptorsChanged() {
|
||||||
const QString CAPACITY = "security.maximum_user_capacity";
|
const QString CAPACITY = "security.maximum_user_capacity";
|
||||||
|
auto settings = static_cast<DomainServer*>(parent())->_settingsManager.getSettingsMap();
|
||||||
const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY);
|
const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY);
|
||||||
unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0;
|
unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0;
|
||||||
|
|
||||||
// TODO: Keep parity with ACL development.
|
auto descriptors = settings[DESCRIPTORS].toMap();
|
||||||
const QString RESTRICTION = "security.restricted_access";
|
descriptors[Descriptors::CAPACITY] = capacity;
|
||||||
const QString RESTRICTION_OPEN = "open";
|
_metadata[DESCRIPTORS] = descriptors;
|
||||||
// const QString RESTRICTION_HIFI = "hifi";
|
|
||||||
const QString RESTRICTION_ACL = "acl";
|
|
||||||
const QVariant* isRestrictedVariant = valueForKeyPath(settings, RESTRICTION);
|
|
||||||
bool isRestricted = isRestrictedVariant ? isRestrictedVariant->toBool() : false;
|
|
||||||
QString restriction = isRestricted ? RESTRICTION_ACL : RESTRICTION_OPEN;
|
|
||||||
|
|
||||||
QVariantMap descriptors = settings[DESCRIPTORS].toMap();
|
// update overwritten fields
|
||||||
descriptors[DESCRIPTORS_CAPACITY] = capacity;
|
securityChanged(false);
|
||||||
descriptors[DESCRIPTORS_RESTRICTION] = restriction;
|
|
||||||
|
#if DEV_BUILD || PR_BUILD
|
||||||
|
qDebug() << "Domain metadata descriptors set:" << _metadata[DESCRIPTORS];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
sendDescriptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainMetadata::securityChanged(bool send) {
|
||||||
|
const QString RESTRICTION_OPEN = "open";
|
||||||
|
const QString RESTRICTION_ANON = "anon";
|
||||||
|
const QString RESTRICTION_HIFI = "hifi";
|
||||||
|
const QString RESTRICTION_ACL = "acl";
|
||||||
|
|
||||||
|
QString restriction;
|
||||||
|
|
||||||
|
const auto& settingsManager = static_cast<DomainServer*>(parent())->_settingsManager;
|
||||||
|
bool hasAnonymousAccess =
|
||||||
|
settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous).canConnectToDomain;
|
||||||
|
bool hasHifiAccess =
|
||||||
|
settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).canConnectToDomain;
|
||||||
|
if (hasAnonymousAccess) {
|
||||||
|
restriction = hasHifiAccess ? RESTRICTION_OPEN : RESTRICTION_ANON;
|
||||||
|
} else if (hasHifiAccess) {
|
||||||
|
restriction = RESTRICTION_HIFI;
|
||||||
|
} else {
|
||||||
|
restriction = RESTRICTION_ACL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto descriptors = _metadata[DESCRIPTORS].toMap();
|
||||||
|
descriptors[Descriptors::RESTRICTION] = restriction;
|
||||||
_metadata[DESCRIPTORS] = descriptors;
|
_metadata[DESCRIPTORS] = descriptors;
|
||||||
|
|
||||||
#if DEV_BUILD || PR_BUILD
|
#if DEV_BUILD || PR_BUILD
|
||||||
qDebug() << "Domain metadata descriptors set:" << descriptors;
|
qDebug() << "Domain metadata restriction set:" << restriction;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (send) {
|
||||||
|
sendDescriptors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainMetadata::usersChanged() {
|
||||||
|
++_tic;
|
||||||
|
|
||||||
|
#if DEV_BUILD || PR_BUILD
|
||||||
|
qDebug() << "Domain metadata users change detected";
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void DomainMetadata::updateUsers() {
|
void DomainMetadata::maybeUpdateUsers() {
|
||||||
|
if (_lastTic == _tic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastTic = _tic;
|
||||||
|
|
||||||
static const QString DEFAULT_HOSTNAME = "*";
|
static const QString DEFAULT_HOSTNAME = "*";
|
||||||
|
|
||||||
auto nodeList = DependencyManager::get<LimitedNodeList>();
|
auto nodeList = DependencyManager::get<LimitedNodeList>();
|
||||||
|
@ -113,20 +177,26 @@ void DomainMetadata::updateUsers() {
|
||||||
});
|
});
|
||||||
|
|
||||||
QVariantMap users = {
|
QVariantMap users = {
|
||||||
{ USERS_NUM_TOTAL, numConnected },
|
{ Users::NUM_TOTAL, numConnected },
|
||||||
{ USERS_NUM_ANON, numConnectedAnonymously },
|
{ Users::NUM_ANON, numConnectedAnonymously },
|
||||||
{ USERS_HOSTNAMES, userHostnames }};
|
{ Users::HOSTNAMES, userHostnames }};
|
||||||
_metadata[USERS] = users;
|
_metadata[USERS] = users;
|
||||||
|
++_tic;
|
||||||
|
|
||||||
#if DEV_BUILD || PR_BUILD
|
#if DEV_BUILD || PR_BUILD
|
||||||
qDebug() << "Domain metadata users updated:" << users;
|
qDebug() << "Domain metadata users updated:" << users;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void DomainMetadata::usersChanged() {
|
void DomainMetadata::sendDescriptors() {
|
||||||
++_tic;
|
QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(get(DESCRIPTORS)).toJson(QJsonDocument::Compact)));
|
||||||
|
const QUuid& domainID = DependencyManager::get<LimitedNodeList>()->getSessionUUID();
|
||||||
#if DEV_BUILD || PR_BUILD
|
if (!domainID.isNull()) {
|
||||||
qDebug() << "Domain metadata users change detected";
|
static const QString DOMAIN_UPDATE = "/api/v1/domains/%1";
|
||||||
#endif
|
DependencyManager::get<AccountManager>()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)),
|
||||||
|
AccountManagerAuth::Required,
|
||||||
|
QNetworkAccessManager::PutOperation,
|
||||||
|
JSONCallbackParameters(),
|
||||||
|
domainUpdateJSON.toUtf8());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,46 +19,48 @@
|
||||||
class DomainMetadata : public QObject {
|
class DomainMetadata : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
using Tic = uint32_t;
|
||||||
|
|
||||||
static const QString USERS;
|
static const QString USERS;
|
||||||
static const QString USERS_NUM_TOTAL;
|
class Users {
|
||||||
static const QString USERS_NUM_ANON;
|
public:
|
||||||
static const QString USERS_HOSTNAMES;
|
static const QString NUM_TOTAL;
|
||||||
|
static const QString NUM_ANON;
|
||||||
|
static const QString HOSTNAMES;
|
||||||
|
};
|
||||||
|
|
||||||
static const QString DESCRIPTORS;
|
static const QString DESCRIPTORS;
|
||||||
static const QString DESCRIPTORS_DESCRIPTION;
|
class Descriptors {
|
||||||
static const QString DESCRIPTORS_CAPACITY;
|
public:
|
||||||
static const QString DESCRIPTORS_HOURS;
|
static const QString DESCRIPTION;
|
||||||
static const QString DESCRIPTORS_RESTRICTION;
|
static const QString CAPACITY;
|
||||||
static const QString DESCRIPTORS_MATURITY;
|
static const QString HOURS;
|
||||||
static const QString DESCRIPTORS_HOSTS;
|
static const QString RESTRICTION;
|
||||||
static const QString DESCRIPTORS_TAGS;
|
static const QString MATURITY;
|
||||||
static const QString DESCRIPTORS_IMG;
|
static const QString HOSTS;
|
||||||
static const QString DESCRIPTORS_IMG_SRC;
|
static const QString TAGS;
|
||||||
static const QString DESCRIPTORS_IMG_TYPE;
|
};
|
||||||
static const QString DESCRIPTORS_IMG_SIZE;
|
|
||||||
static const QString DESCRIPTORS_IMG_UPDATED_AT;
|
|
||||||
|
|
||||||
public:
|
DomainMetadata(QObject* domainServer);
|
||||||
DomainMetadata();
|
DomainMetadata() = delete;
|
||||||
|
|
||||||
// Returns the last set metadata
|
// Get cached metadata
|
||||||
// If connected users have changed, metadata may need to be updated
|
QJsonObject get();
|
||||||
// this should be checked by storing tic = getTic() between calls
|
QJsonObject get(const QString& group);
|
||||||
// and testing it for equality before the next get (tic == getTic())
|
|
||||||
QJsonObject get() { return QJsonObject::fromVariantMap(_metadata); }
|
|
||||||
QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS].toMap()); }
|
|
||||||
QJsonObject getDescriptors() { return QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); }
|
|
||||||
|
|
||||||
uint32_t getTic() { return _tic; }
|
|
||||||
|
|
||||||
void setDescriptors(QVariantMap& settings);
|
|
||||||
void updateUsers();
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
void descriptorsChanged();
|
||||||
|
void securityChanged(bool send);
|
||||||
|
void securityChanged() { securityChanged(true); }
|
||||||
void usersChanged();
|
void usersChanged();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void maybeUpdateUsers();
|
||||||
|
void sendDescriptors();
|
||||||
|
|
||||||
QVariantMap _metadata;
|
QVariantMap _metadata;
|
||||||
|
uint32_t _lastTic{ -1 };
|
||||||
uint32_t _tic{ 0 };
|
uint32_t _tic{ 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -94,10 +94,6 @@ DomainServer::DomainServer(int argc, char* argv[]) :
|
||||||
qRegisterMetaType<DomainServerWebSessionData>("DomainServerWebSessionData");
|
qRegisterMetaType<DomainServerWebSessionData>("DomainServerWebSessionData");
|
||||||
qRegisterMetaTypeStreamOperators<DomainServerWebSessionData>("DomainServerWebSessionData");
|
qRegisterMetaTypeStreamOperators<DomainServerWebSessionData>("DomainServerWebSessionData");
|
||||||
|
|
||||||
// update the metadata when a user (dis)connects
|
|
||||||
connect(this, &DomainServer::userConnected, &_metadata, &DomainMetadata::usersChanged);
|
|
||||||
connect(this, &DomainServer::userDisconnected, &_metadata, &DomainMetadata::usersChanged);
|
|
||||||
|
|
||||||
// make sure we hear about newly connected nodes from our gatekeeper
|
// make sure we hear about newly connected nodes from our gatekeeper
|
||||||
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
|
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
|
||||||
|
|
||||||
|
@ -108,9 +104,6 @@ DomainServer::DomainServer(int argc, char* argv[]) :
|
||||||
connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
|
connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
|
||||||
&_gatekeeper, &DomainGatekeeper::updateNodePermissions);
|
&_gatekeeper, &DomainGatekeeper::updateNodePermissions);
|
||||||
|
|
||||||
// update the metadata with current descriptors
|
|
||||||
_metadata.setDescriptors(_settingsManager.getSettingsMap());
|
|
||||||
|
|
||||||
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
|
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
|
||||||
// we either read a certificate and private key or were not passed one
|
// we either read a certificate and private key or were not passed one
|
||||||
// and completed login or did not need to
|
// and completed login or did not need to
|
||||||
|
@ -125,17 +118,9 @@ DomainServer::DomainServer(int argc, char* argv[]) :
|
||||||
_gatekeeper.preloadAllowedUserPublicKeys();
|
_gatekeeper.preloadAllowedUserPublicKeys();
|
||||||
|
|
||||||
optionallyGetTemporaryName(args);
|
optionallyGetTemporaryName(args);
|
||||||
|
|
||||||
// send metadata descriptors
|
|
||||||
QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(_metadata.getDescriptors()).toJson(QJsonDocument::Compact)));
|
|
||||||
const QUuid& domainID = DependencyManager::get<LimitedNodeList>()->getSessionUUID();
|
|
||||||
static const QString DOMAIN_UPDATE = "/api/v1/domains/%1";
|
|
||||||
DependencyManager::get<AccountManager>()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)),
|
|
||||||
AccountManagerAuth::Required,
|
|
||||||
QNetworkAccessManager::PutOperation,
|
|
||||||
JSONCallbackParameters(),
|
|
||||||
domainUpdateJSON.toUtf8());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_metadata = new DomainMetadata(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
DomainServer::~DomainServer() {
|
DomainServer::~DomainServer() {
|
||||||
|
@ -1111,14 +1096,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) {
|
||||||
NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous);
|
NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous);
|
||||||
domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain;
|
domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain;
|
||||||
|
|
||||||
// Add the metadata to the heartbeat
|
if (_metadata) {
|
||||||
static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat";
|
// Add the metadata to the heartbeat
|
||||||
auto tic = _metadata.getTic();
|
static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat";
|
||||||
if (_metadataTic != tic) {
|
domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata->get(DomainMetadata::USERS);
|
||||||
_metadataTic = tic;
|
|
||||||
_metadata.updateUsers();
|
|
||||||
}
|
}
|
||||||
domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.getUsers();
|
|
||||||
|
|
||||||
QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact)));
|
QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact)));
|
||||||
|
|
||||||
|
|
|
@ -172,13 +172,12 @@ private:
|
||||||
|
|
||||||
DomainServerSettingsManager _settingsManager;
|
DomainServerSettingsManager _settingsManager;
|
||||||
|
|
||||||
DomainMetadata _metadata;
|
|
||||||
uint32_t _metadataTic{ 0 };
|
|
||||||
|
|
||||||
HifiSockAddr _iceServerSocket;
|
HifiSockAddr _iceServerSocket;
|
||||||
std::unique_ptr<NLPacket> _iceServerHeartbeatPacket;
|
std::unique_ptr<NLPacket> _iceServerHeartbeatPacket;
|
||||||
|
|
||||||
QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer
|
// These will be parented to this, they are not dangling
|
||||||
|
DomainMetadata* _metadata { nullptr };
|
||||||
|
QTimer* _iceHeartbeatTimer { nullptr };
|
||||||
|
|
||||||
QList<QHostAddress> _iceServerAddresses;
|
QList<QHostAddress> _iceServerAddresses;
|
||||||
QSet<QHostAddress> _failedIceServerAddresses;
|
QSet<QHostAddress> _failedIceServerAddresses;
|
||||||
|
@ -190,6 +189,7 @@ private:
|
||||||
bool _hasAccessToken { false };
|
bool _hasAccessToken { false };
|
||||||
|
|
||||||
friend class DomainGatekeeper;
|
friend class DomainGatekeeper;
|
||||||
|
friend class DomainMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue