|
@@ -70,8 +70,20 @@ import org.apache.hadoop.ozone.om.ratis.OzoneManagerRatisServer;
|
|
|
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OzoneAclInfo;
|
|
|
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ServicePort;
|
|
|
import org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
|
|
|
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
|
|
|
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
|
|
|
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType;
|
|
|
+import org.apache.hadoop.ozone.security.acl.OzoneAccessAuthorizer;
|
|
|
+import org.apache.hadoop.ozone.security.acl.OzoneAclException;
|
|
|
+import org.apache.hadoop.ozone.security.acl.OzoneAclException.ErrorCode;
|
|
|
+import org.apache.hadoop.ozone.security.acl.OzoneObj;
|
|
|
+import org.apache.hadoop.ozone.security.acl.OzoneObj.StoreType;
|
|
|
+import org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType;
|
|
|
+import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
|
|
|
+import org.apache.hadoop.ozone.security.acl.RequestContext;
|
|
|
import org.apache.hadoop.security.UserGroupInformation;
|
|
|
import org.apache.hadoop.util.GenericOptionsParser;
|
|
|
+import org.apache.hadoop.util.ReflectionUtils;
|
|
|
import org.apache.hadoop.util.ShutdownHookManager;
|
|
|
import org.apache.hadoop.util.StringUtils;
|
|
|
import org.apache.ratis.util.LifeCycle;
|
|
@@ -103,6 +115,9 @@ import static org.apache.hadoop.hdds.HddsUtils.isHddsEnabled;
|
|
|
import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeState.HEALTHY;
|
|
|
import static org.apache.hadoop.hdds.server.ServerUtils.updateRPCListenAddress;
|
|
|
import static org.apache.hadoop.ozone.OmUtils.getOmAddress;
|
|
|
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ACL_AUTHORIZER_CLASS;
|
|
|
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ACL_ENABLED;
|
|
|
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ACL_ENABLED_DEFAULT;
|
|
|
import static org.apache.hadoop.ozone.OzoneConsts.OM_METRICS_FILE;
|
|
|
import static org.apache.hadoop.ozone.OzoneConsts.OM_METRICS_TEMP_FILE;
|
|
|
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ENABLED;
|
|
@@ -159,6 +174,8 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
private static final int SHUTDOWN_HOOK_PRIORITY = 30;
|
|
|
private final Runnable shutdownHook;
|
|
|
private final File omMetaDir;
|
|
|
+ private final boolean isAclEnabled;
|
|
|
+ private final IAccessAuthorizer accessAuthorizer;
|
|
|
|
|
|
private OzoneManager(OzoneConfiguration conf) throws IOException {
|
|
|
Preconditions.checkNotNull(conf);
|
|
@@ -198,11 +215,32 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
};
|
|
|
ShutdownHookManager.get().addShutdownHook(shutdownHook,
|
|
|
SHUTDOWN_HOOK_PRIORITY);
|
|
|
-
|
|
|
+ isAclEnabled = conf.getBoolean(OZONE_ACL_ENABLED,
|
|
|
+ OZONE_ACL_ENABLED_DEFAULT);
|
|
|
+ if (isAclEnabled) {
|
|
|
+ accessAuthorizer = getACLAuthorizerInstance(conf);
|
|
|
+ } else {
|
|
|
+ accessAuthorizer = null;
|
|
|
+ }
|
|
|
omMetaDir = OmUtils.getOmDbDir(configuration);
|
|
|
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Returns an instance of {@link IAccessAuthorizer}.
|
|
|
+ * Looks up the configuration to see if there is custom class specified.
|
|
|
+ * Constructs the instance by passing the configuration directly to the
|
|
|
+ * constructor to achieve thread safety using final fields.
|
|
|
+ * @param conf
|
|
|
+ * @return IAccessAuthorizer
|
|
|
+ */
|
|
|
+ private IAccessAuthorizer getACLAuthorizerInstance(OzoneConfiguration conf) {
|
|
|
+ Class<? extends IAccessAuthorizer> clazz = conf.getClass(
|
|
|
+ OZONE_ACL_AUTHORIZER_CLASS, OzoneAccessAuthorizer.class,
|
|
|
+ IAccessAuthorizer.class);
|
|
|
+ return ReflectionUtils.newInstance(clazz, conf);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Class which schedule saving metrics to a file.
|
|
|
*/
|
|
@@ -646,6 +684,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public void createVolume(OmVolumeArgs args) throws IOException {
|
|
|
try {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE,
|
|
|
+ ACLType.CREATE, args.getVolume(), null, null);
|
|
|
+ }
|
|
|
metrics.incNumVolumeCreates();
|
|
|
volumeManager.createVolume(args);
|
|
|
AUDIT.logWriteSuccess(buildAuditMessageForSuccess(OMAction.CREATE_VOLUME,
|
|
@@ -661,6 +703,46 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Checks if current caller has acl permissions.
|
|
|
+ *
|
|
|
+ * @param resType - Type of ozone resource. Ex volume, bucket.
|
|
|
+ * @param store - Store type. i.e Ozone, S3.
|
|
|
+ * @param acl - type of access to be checked.
|
|
|
+ * @param vol - name of volume
|
|
|
+ * @param bucket - bucket name
|
|
|
+ * @param key - key
|
|
|
+ * @throws OzoneAclException
|
|
|
+ */
|
|
|
+ private void checkAcls(ResourceType resType, StoreType store,
|
|
|
+ ACLType acl, String vol, String bucket, String key)
|
|
|
+ throws OzoneAclException {
|
|
|
+ if(!isAclEnabled) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ OzoneObj obj = OzoneObjInfo.Builder.newBuilder()
|
|
|
+ .setResType(resType)
|
|
|
+ .setStoreType(store)
|
|
|
+ .setVolumeName(vol)
|
|
|
+ .setBucketName(bucket)
|
|
|
+ .setKeyName(key).build();
|
|
|
+ UserGroupInformation user = ProtobufRpcEngine.Server.getRemoteUser();
|
|
|
+ RequestContext context = RequestContext.newBuilder()
|
|
|
+ .setClientUgi(user)
|
|
|
+ .setIp(ProtobufRpcEngine.Server.getRemoteIp())
|
|
|
+ .setAclType(ACLIdentityType.USER)
|
|
|
+ .setAclRights(acl)
|
|
|
+ .build();
|
|
|
+ if (!accessAuthorizer.checkAccess(obj, context)) {
|
|
|
+ LOG.warn("User {} doesn't have {} permission to access {}",
|
|
|
+ user.getUserName(), acl, resType);
|
|
|
+ throw new OzoneAclException("User " + user.getUserName() + " doesn't " +
|
|
|
+ "have " + acl + " permission to access " + resType,
|
|
|
+ ErrorCode.PERMISSION_DENIED);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Changes the owner of a volume.
|
|
|
*
|
|
@@ -670,6 +752,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
*/
|
|
|
@Override
|
|
|
public void setOwner(String volume, String owner) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE, ACLType.WRITE_ACL, volume,
|
|
|
+ null, null);
|
|
|
+ }
|
|
|
Map<String, String> auditMap = buildAuditMap(volume);
|
|
|
auditMap.put(OzoneConsts.OWNER, owner);
|
|
|
try {
|
|
@@ -695,6 +781,11 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
*/
|
|
|
@Override
|
|
|
public void setQuota(String volume, long quota) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE, ACLType.WRITE, volume,
|
|
|
+ null, null);
|
|
|
+ }
|
|
|
+
|
|
|
Map<String, String> auditMap = buildAuditMap(volume);
|
|
|
auditMap.put(OzoneConsts.QUOTA, String.valueOf(quota));
|
|
|
try {
|
|
@@ -722,6 +813,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public boolean checkVolumeAccess(String volume, OzoneAclInfo userAcl)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE,
|
|
|
+ ACLType.READ, volume, null, null);
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = buildAuditMap(volume);
|
|
|
auditMap.put(OzoneConsts.USER_ACL,
|
|
@@ -752,6 +847,11 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
*/
|
|
|
@Override
|
|
|
public OmVolumeArgs getVolumeInfo(String volume) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE, ACLType.READ, volume,
|
|
|
+ null, null);
|
|
|
+ }
|
|
|
+
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = buildAuditMap(volume);
|
|
|
try {
|
|
@@ -780,6 +880,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public void deleteVolume(String volume) throws IOException {
|
|
|
try {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE, ACLType.DELETE, volume,
|
|
|
+ null, null);
|
|
|
+ }
|
|
|
metrics.incNumVolumeDeletes();
|
|
|
volumeManager.deleteVolume(volume);
|
|
|
AUDIT.logWriteSuccess(buildAuditMessageForSuccess(OMAction.DELETE_VOLUME,
|
|
@@ -807,6 +911,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public List<OmVolumeArgs> listVolumeByUser(String userName, String prefix,
|
|
|
String prevKey, int maxKeys) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE, ACLType.LIST, prefix,
|
|
|
+ null, null);
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = new LinkedHashMap<>();
|
|
|
auditMap.put(OzoneConsts.PREV_KEY, prevKey);
|
|
@@ -843,6 +951,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public List<OmVolumeArgs> listAllVolumes(String prefix, String prevKey, int
|
|
|
maxKeys) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE, ACLType.LIST, prefix,
|
|
|
+ null, null);
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = new LinkedHashMap<>();
|
|
|
auditMap.put(OzoneConsts.PREV_KEY, prevKey);
|
|
@@ -875,6 +987,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public void createBucket(OmBucketInfo bucketInfo) throws IOException {
|
|
|
try {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.BUCKET, StoreType.OZONE, ACLType.CREATE,
|
|
|
+ bucketInfo.getVolumeName(), bucketInfo.getBucketName(), null);
|
|
|
+ }
|
|
|
metrics.incNumBucketCreates();
|
|
|
bucketManager.createBucket(bucketInfo);
|
|
|
AUDIT.logWriteSuccess(buildAuditMessageForSuccess(OMAction.CREATE_BUCKET,
|
|
@@ -895,6 +1011,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
public List<OmBucketInfo> listBuckets(String volumeName,
|
|
|
String startKey, String prefix, int maxNumOfBuckets)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.OZONE, ACLType.LIST, volumeName,
|
|
|
+ null, null);
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = buildAuditMap(volumeName);
|
|
|
auditMap.put(OzoneConsts.START_KEY, startKey);
|
|
@@ -930,6 +1050,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public OmBucketInfo getBucketInfo(String volume, String bucket)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.BUCKET, StoreType.OZONE, ACLType.READ, volume,
|
|
|
+ bucket, null);
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = buildAuditMap(volume);
|
|
|
auditMap.put(OzoneConsts.BUCKET, bucket);
|
|
@@ -959,6 +1083,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
*/
|
|
|
@Override
|
|
|
public OpenKeySession openKey(OmKeyArgs args) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.READ,
|
|
|
+ args.getVolumeName(), args.getBucketName(), args.getKeyName());
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
try {
|
|
|
metrics.incNumKeyAllocates();
|
|
@@ -980,6 +1108,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public void commitKey(OmKeyArgs args, long clientID)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.WRITE,
|
|
|
+ args.getVolumeName(), args.getBucketName(), args.getKeyName());
|
|
|
+ }
|
|
|
Map<String, String> auditMap = (args == null) ? new LinkedHashMap<>() :
|
|
|
args.toAuditMap();
|
|
|
auditMap.put(OzoneConsts.CLIENT_ID, String.valueOf(clientID));
|
|
@@ -1009,6 +1141,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public OmKeyLocationInfo allocateBlock(OmKeyArgs args, long clientID)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.WRITE,
|
|
|
+ args.getVolumeName(), args.getBucketName(), args.getKeyName());
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = (args == null) ? new LinkedHashMap<>() :
|
|
|
args.toAuditMap();
|
|
@@ -1039,6 +1175,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
*/
|
|
|
@Override
|
|
|
public OmKeyInfo lookupKey(OmKeyArgs args) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.READ,
|
|
|
+ args.getVolumeName(), args.getBucketName(), args.getKeyName());
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
try {
|
|
|
metrics.incNumKeyLookups();
|
|
@@ -1059,6 +1199,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
|
|
|
@Override
|
|
|
public void renameKey(OmKeyArgs args, String toKeyName) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.WRITE,
|
|
|
+ args.getVolumeName(), args.getBucketName(), args.getKeyName());
|
|
|
+ }
|
|
|
Map<String, String> auditMap = (args == null) ? new LinkedHashMap<>() :
|
|
|
args.toAuditMap();
|
|
|
auditMap.put(OzoneConsts.TO_KEY_NAME, toKeyName);
|
|
@@ -1084,6 +1228,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public void deleteKey(OmKeyArgs args) throws IOException {
|
|
|
try {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.DELETE,
|
|
|
+ args.getVolumeName(), args.getBucketName(), args.getKeyName());
|
|
|
+ }
|
|
|
metrics.incNumKeyDeletes();
|
|
|
keyManager.deleteKey(args);
|
|
|
AUDIT.logWriteSuccess(buildAuditMessageForSuccess(OMAction.DELETE_KEY,
|
|
@@ -1100,6 +1248,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public List<OmKeyInfo> listKeys(String volumeName, String bucketName,
|
|
|
String startKey, String keyPrefix, int maxKeys) throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.LIST, volumeName,
|
|
|
+ bucketName, keyPrefix);
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = buildAuditMap(volumeName);
|
|
|
auditMap.put(OzoneConsts.BUCKET, bucketName);
|
|
@@ -1133,6 +1285,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
@Override
|
|
|
public void setBucketProperty(OmBucketArgs args)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.BUCKET, StoreType.OZONE, ACLType.WRITE,
|
|
|
+ args.getVolumeName(), args.getBucketName(), null);
|
|
|
+ }
|
|
|
try {
|
|
|
metrics.incNumBucketUpdates();
|
|
|
bucketManager.setBucketProperty(args);
|
|
@@ -1155,6 +1311,8 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
*/
|
|
|
@Override
|
|
|
public void deleteBucket(String volume, String bucket) throws IOException {
|
|
|
+ checkAcls(ResourceType.BUCKET, StoreType.OZONE, ACLType.WRITE, volume,
|
|
|
+ bucket, null);
|
|
|
Map<String, String> auditMap = buildAuditMap(volume);
|
|
|
auditMap.put(OzoneConsts.BUCKET, bucket);
|
|
|
try {
|
|
@@ -1302,6 +1460,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
public void createS3Bucket(String userName, String s3BucketName)
|
|
|
throws IOException {
|
|
|
try {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.BUCKET, StoreType.S3, ACLType.CREATE,
|
|
|
+ null, s3BucketName, null);
|
|
|
+ }
|
|
|
metrics.incNumBucketCreates();
|
|
|
try {
|
|
|
boolean newVolumeCreate = s3BucketManager.createOzoneVolumeIfNeeded(
|
|
@@ -1331,9 +1493,12 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
/**
|
|
|
* {@inheritDoc}
|
|
|
*/
|
|
|
- public void deleteS3Bucket(String s3BucketName)
|
|
|
- throws IOException {
|
|
|
+ public void deleteS3Bucket(String s3BucketName) throws IOException {
|
|
|
try {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.BUCKET, StoreType.S3, ACLType.DELETE, null,
|
|
|
+ s3BucketName, null);
|
|
|
+ }
|
|
|
metrics.incNumBucketDeletes();
|
|
|
s3BucketManager.deleteS3Bucket(s3BucketName);
|
|
|
metrics.decNumBuckets();
|
|
@@ -1348,6 +1513,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
*/
|
|
|
public String getOzoneBucketMapping(String s3BucketName)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.BUCKET, StoreType.S3, ACLType.READ,
|
|
|
+ null, s3BucketName, null);
|
|
|
+ }
|
|
|
return s3BucketManager.getOzoneBucketMapping(s3BucketName);
|
|
|
}
|
|
|
|
|
@@ -1355,6 +1524,10 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
public List<OmBucketInfo> listS3Buckets(String userName, String startKey,
|
|
|
String prefix, int maxNumOfBuckets)
|
|
|
throws IOException {
|
|
|
+ if(isAclEnabled) {
|
|
|
+ checkAcls(ResourceType.VOLUME, StoreType.S3, ACLType.LIST,
|
|
|
+ s3BucketManager.getOzoneVolumeNameForUser(userName), null, null);
|
|
|
+ }
|
|
|
boolean auditSuccess = true;
|
|
|
Map<String, String> auditMap = buildAuditMap(userName);
|
|
|
auditMap.put(OzoneConsts.START_KEY, startKey);
|
|
@@ -1410,4 +1583,8 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
|
|
|
return name;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ public static Logger getLogger() {
|
|
|
+ return LOG;
|
|
|
+ }
|
|
|
}
|