2008-11-17 18:29:00 -05:00
|
|
|
// ----------------------------------------------------------------------------
|
2008-03-05 09:52:00 -05:00
|
|
|
//
|
|
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
|
|
//
|
2008-11-17 18:29:00 -05:00
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
// #define APPLY_TPM
|
2008-03-05 09:52:00 -05:00
|
|
|
|
|
|
|
namespace Microsoft.Singularity.Security
|
|
|
|
{
|
|
|
|
using System;
|
|
|
|
using System.Threading;
|
|
|
|
using System.Collections;
|
|
|
|
using System.Text;
|
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
using Microsoft.Contracts;
|
2008-03-05 09:52:00 -05:00
|
|
|
using Microsoft.Singularity.Loader;
|
|
|
|
using Microsoft.Singularity.Xml;
|
|
|
|
using Microsoft.Singularity.Io;
|
2008-11-17 18:29:00 -05:00
|
|
|
using Microsoft.Singularity.Tpm;
|
2008-03-05 09:52:00 -05:00
|
|
|
|
|
|
|
public class PrincipalImpl
|
|
|
|
{
|
|
|
|
private const string KernelName = "kernel.localhost";
|
|
|
|
private const string UnknownName = "unknown";
|
|
|
|
private const string UnknownPublisher = "unknown";
|
|
|
|
private const string DefaultNameContext = "localhost";
|
|
|
|
private const string TruncateHistoryPrivilege = "$truncate-history-privilege.localhost";
|
|
|
|
|
|
|
|
// security tags in config XML
|
|
|
|
const string AuthPolicyXmlTag = "authpolicy";
|
|
|
|
const string SubexprXmlTag = "subexpr";
|
|
|
|
const string SubexprNameXmlAttribute = "name";
|
|
|
|
const string SubexprExpansionXmlAttribute = "expansion";
|
|
|
|
const string AuthorityXmlTag = "authority";
|
|
|
|
const string AuthorityNameXmlAttribute = "name";
|
|
|
|
const string AuthorityAclXmlAttribute = "acl";
|
|
|
|
|
|
|
|
private static Hashtable! principalHT; // map from principal Val (ulong) to PrincipalT
|
|
|
|
private static Hashtable! exprHT; // map from subexpr name to description (both strings)
|
|
|
|
private static Hashtable! privHT; // map from privilege name to ArrayList of PrincipalT
|
|
|
|
private static Hashtable! authorityHT; // map from authority name to acl expansion (both strings)
|
|
|
|
private static Hashtable! publisherHT; // map from publisher name to PrincipalT
|
|
|
|
|
|
|
|
private const char CharInvocation = '+';
|
|
|
|
private const char CharRole = '@';
|
|
|
|
private const string NullName = "";
|
|
|
|
|
|
|
|
private const ulong KernelIdNum = 1;
|
|
|
|
private static ulong nextIdVal = KernelIdNum + (ulong)1;
|
|
|
|
private static SecurityDiagnostics! secDiag;
|
|
|
|
private static AclCore! aclCore;
|
|
|
|
|
|
|
|
|
|
|
|
internal class PrincipalT {
|
2008-11-17 18:29:00 -05:00
|
|
|
internal ulong val;
|
2008-03-05 09:52:00 -05:00
|
|
|
internal string! name;
|
2008-11-17 18:29:00 -05:00
|
|
|
|
|
|
|
[NotDelayed]
|
|
|
|
internal PrincipalT(string! _name)
|
|
|
|
{
|
|
|
|
name = _name;
|
|
|
|
base();
|
|
|
|
lock (principalHT) {
|
|
|
|
val = nextIdVal++;
|
|
|
|
principalHT.Add(val, this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
[NotDelayed]
|
|
|
|
internal PrincipalT(string! _name, ManifestPrincipal! mpT, string! delegatorName)
|
|
|
|
{
|
|
|
|
name = _name;
|
|
|
|
base();
|
|
|
|
lock (principalHT) {
|
|
|
|
// in this case we want the record a new delegate under the same lock
|
|
|
|
// thus the two tables will be consistent
|
|
|
|
val = nextIdVal++;
|
|
|
|
principalHT.Add(val, this);
|
|
|
|
mpT.delegates[delegatorName] = val;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal class ManifestPrincipal : PrincipalT {
|
|
|
|
internal string! manifestName;
|
2008-03-05 09:52:00 -05:00
|
|
|
internal string[] privs;
|
|
|
|
internal PrincipalT publisherT;
|
|
|
|
internal ArrayList principalHashes;
|
2008-11-17 18:29:00 -05:00
|
|
|
internal Hashtable! delegates;
|
2008-03-05 09:52:00 -05:00
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
[NotDelayed]
|
|
|
|
internal ManifestPrincipal(string! _name, string! _maniName, string[] _privs, PrincipalT _publisherT)
|
2008-03-05 09:52:00 -05:00
|
|
|
{
|
|
|
|
manifestName = _maniName;
|
|
|
|
privs = _privs;
|
|
|
|
publisherT = _publisherT;
|
|
|
|
principalHashes = null;
|
2008-11-17 18:29:00 -05:00
|
|
|
delegates = new Hashtable();
|
|
|
|
base(_name);
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
[NotDelayed]
|
|
|
|
internal ManifestPrincipal(string! _name, string! _maniName, string[] _privs,
|
|
|
|
PrincipalT _publisherT, ArrayList _principalHashes)
|
2008-03-05 09:52:00 -05:00
|
|
|
{
|
|
|
|
manifestName = _maniName;
|
|
|
|
privs = _privs;
|
|
|
|
publisherT = _publisherT;
|
|
|
|
principalHashes = _principalHashes;
|
2008-11-17 18:29:00 -05:00
|
|
|
delegates = new Hashtable();
|
|
|
|
base(_name);
|
|
|
|
}
|
|
|
|
|
|
|
|
internal PrincipalT! MintDelegate(Principal delegator)
|
|
|
|
{
|
|
|
|
string delegatorName = delegator.GetName();
|
|
|
|
if (delegatorName == NullName) {
|
|
|
|
throw new Exception("NewDelegation with null delegator");
|
|
|
|
}
|
|
|
|
PrincipalT dpT = this.delegates[delegatorName] as PrincipalT;
|
|
|
|
if (dpT != null) {
|
|
|
|
return dpT;
|
|
|
|
}
|
|
|
|
int newlength = delegatorName.Length + this.manifestName.Length + 2;
|
|
|
|
StringBuilder sb = new StringBuilder(delegatorName, newlength);
|
|
|
|
sb.Append(CharInvocation);
|
|
|
|
sb.Append(this.manifestName);
|
|
|
|
return new PrincipalT(sb.ToString(), this, delegatorName);
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void Initialize(XmlNode! config)
|
|
|
|
{
|
|
|
|
// kernel publisher T is currently "unknown". fix this (maybe)
|
|
|
|
// one way to fix it is to have kernel manifest here, but that
|
|
|
|
// isn't currently possible.
|
|
|
|
principalHT = new Hashtable();
|
|
|
|
exprHT = new Hashtable();
|
|
|
|
privHT = new Hashtable();
|
|
|
|
authorityHT = new Hashtable();
|
|
|
|
publisherHT = new Hashtable();
|
|
|
|
secDiag = new SecurityDiagnostics();
|
|
|
|
aclCore = new AclCore("PrincipalImpl", null);
|
|
|
|
InstallPolicy(config);
|
|
|
|
principalHT.Add(KernelIdNum,
|
2008-11-17 18:29:00 -05:00
|
|
|
new ManifestPrincipal(KernelName, KernelName, null, GetPublisherT(UnknownPublisher)));
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Principal Self()
|
|
|
|
{
|
|
|
|
return new Principal(KernelIdNum);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static Principal MakePrincipal(ulong id)
|
|
|
|
{
|
|
|
|
return new Principal(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static string! GetPrincipalName(Principal pr)
|
|
|
|
{
|
|
|
|
PrincipalT pT = (PrincipalT) principalHT[pr.Val];
|
2008-11-17 18:29:00 -05:00
|
|
|
if (pT == null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
return NullName;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
return pT.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static ArrayList GetPrincipalHashes(Principal pr)
|
|
|
|
{
|
2008-11-17 18:29:00 -05:00
|
|
|
ManifestPrincipal mpT = principalHT[pr.Val] as ManifestPrincipal;
|
|
|
|
if (mpT == null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
return new ArrayList();
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
return mpT.principalHashes;
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
public static Principal NewInvocation(Principal parent, Manifest! manifest, string role, IoMemory rawImage)
|
|
|
|
{
|
|
|
|
string manifestName = manifest.Name;
|
|
|
|
string publisher = manifest.Publisher;
|
|
|
|
string name, newname;
|
|
|
|
bool eraseHistory = false;
|
|
|
|
string[] appPrivs = manifest.Privileges;
|
|
|
|
byte[] imageHash = null;
|
|
|
|
ArrayList newprincipalHashes = null;
|
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
if (manifestName == null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
manifestName = UnknownName;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
if (publisher == null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
publisher = UnknownPublisher;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (rawImage != null) {
|
|
|
|
#if APPLY_TPM
|
|
|
|
byte[] dataToHash = new byte[rawImage.Length];
|
|
|
|
|
|
|
|
rawImage.Read8(0,
|
|
|
|
dataToHash,
|
|
|
|
0,
|
|
|
|
rawImage.Length);
|
|
|
|
|
|
|
|
imageHash = SHA1.Hash(dataToHash);
|
|
|
|
#endif // APPLY_TPM
|
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
|
|
|
|
PrincipalT publisherT = GetPublisherT(publisher);
|
|
|
|
|
|
|
|
manifestName = String.Format("{0}.{1}", manifestName, publisher);
|
|
|
|
name = manifestName;
|
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
ArrayList arrarr = EvaluatePrivileges(appPrivs, publisherT, out eraseHistory);
|
2008-03-05 09:52:00 -05:00
|
|
|
if (eraseHistory || (parent.Val == KernelIdNum)) {
|
|
|
|
// ignore role if eraseHistory or kernel parent
|
|
|
|
newname = name;
|
|
|
|
if (imageHash != null) {
|
|
|
|
newprincipalHashes = new ArrayList();
|
|
|
|
newprincipalHashes.Add(imageHash);
|
|
|
|
}
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
else {
|
2008-03-05 09:52:00 -05:00
|
|
|
string parentName = parent.GetName();
|
2008-11-17 18:29:00 -05:00
|
|
|
if (parentName == NullName) {
|
2008-03-05 09:52:00 -05:00
|
|
|
throw new Exception("NewInvocation with null parent");
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
int newlength = parentName.Length + name.Length + 2;
|
2008-11-17 18:29:00 -05:00
|
|
|
if (role != null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
newlength += (role.Length + 1);
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
StringBuilder sb = new StringBuilder(parentName, newlength);
|
|
|
|
if (role != null) {
|
|
|
|
sb.Append(CharRole);
|
|
|
|
sb.Append(role);
|
|
|
|
}
|
|
|
|
sb.Append(CharInvocation);
|
|
|
|
sb.Append(name);
|
|
|
|
newname = sb.ToString();
|
|
|
|
|
|
|
|
if (imageHash != null) {
|
|
|
|
ArrayList parentHashes = parent.GetHashes();
|
|
|
|
newprincipalHashes = new ArrayList(parentHashes);
|
|
|
|
newprincipalHashes.Add(imageHash);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
PrincipalT pT = new ManifestPrincipal(newname, manifestName, appPrivs, publisherT, newprincipalHashes);
|
2008-03-05 09:52:00 -05:00
|
|
|
if (arrarr != null) {
|
|
|
|
foreach (Object! obj in arrarr) {
|
|
|
|
ArrayList arr = (ArrayList) obj;
|
|
|
|
lock (arr) {
|
|
|
|
arr.Add(pT);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2008-11-17 18:29:00 -05:00
|
|
|
return new Principal(pT.val);
|
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
public static Principal NewDelegation(Principal delegator, Principal target)
|
|
|
|
{
|
|
|
|
ManifestPrincipal targetT = principalHT[target.Val] as ManifestPrincipal;
|
|
|
|
if (targetT == null) {
|
|
|
|
throw new Exception("Delegation with inappropriate target");
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
2008-11-17 18:29:00 -05:00
|
|
|
PrincipalT pT = targetT.MintDelegate(delegator);
|
2008-03-05 09:52:00 -05:00
|
|
|
return new Principal(pT.val);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void Dispose(Principal pr)
|
|
|
|
{
|
2008-11-17 18:29:00 -05:00
|
|
|
ManifestPrincipal mpT = principalHT[pr.Val] as ManifestPrincipal;
|
|
|
|
lock (principalHT) {
|
2008-03-05 09:52:00 -05:00
|
|
|
principalHT.Remove(pr.Val);
|
2008-11-17 18:29:00 -05:00
|
|
|
if (mpT == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
IDictionaryEnumerator myEnumerator = mpT.delegates.GetEnumerator();
|
|
|
|
while (myEnumerator.MoveNext()) {
|
|
|
|
ulong delgPrVal = (ulong) (!)myEnumerator.Value;
|
|
|
|
principalHT.Remove(delgPrVal);
|
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
2008-11-17 18:29:00 -05:00
|
|
|
|
|
|
|
if (mpT.privs != null) {
|
|
|
|
for (int i = 0; i < mpT.privs.Length; i++) {
|
|
|
|
ArrayList arr = (ArrayList) privHT[(!)mpT.privs[i]];
|
2008-03-05 09:52:00 -05:00
|
|
|
if (arr != null) {
|
|
|
|
lock (arr) {
|
2008-11-17 18:29:00 -05:00
|
|
|
arr.Remove(mpT);
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static string ExpandAclIndirection(string! name)
|
|
|
|
{
|
2008-11-17 18:29:00 -05:00
|
|
|
if (name.Length == 0) {
|
2008-03-05 09:52:00 -05:00
|
|
|
return null;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
if (name.IndexOf('.') == -1) {
|
2008-03-05 09:52:00 -05:00
|
|
|
name = String.Format("{0}.{1}", name, DefaultNameContext);
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
|
|
|
|
string res = (string) exprHT[name];
|
2008-11-17 18:29:00 -05:00
|
|
|
if (res != null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
return res;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
|
|
|
|
ArrayList arr = (ArrayList) privHT[name];
|
2008-11-17 18:29:00 -05:00
|
|
|
if (arr == null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
return null;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
lock (arr) {
|
|
|
|
sb.Append("(");
|
2008-11-17 18:29:00 -05:00
|
|
|
for (int i = 0; i < arr.Count; i++) {
|
|
|
|
ManifestPrincipal mpT = (ManifestPrincipal!) arr[i];
|
|
|
|
if (mpT == null || mpT.manifestName == null) {
|
2008-03-05 09:52:00 -05:00
|
|
|
continue;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
if (i != 0) {
|
2008-03-05 09:52:00 -05:00
|
|
|
sb.Append('|');
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
sb.Append(mpT.manifestName);
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
sb.Append(")");
|
|
|
|
}
|
|
|
|
return sb.ToString();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void RegisterAclCore(Object core)
|
|
|
|
{
|
|
|
|
secDiag.RegisterAclCore((AclCore!)core);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void Export()
|
|
|
|
{
|
|
|
|
secDiag.Start();
|
|
|
|
}
|
|
|
|
|
2008-11-17 18:29:00 -05:00
|
|
|
private static ArrayList EvaluatePrivileges(string[] appPrivs, PrincipalT! publisherT, out bool eraseHistory)
|
|
|
|
{
|
|
|
|
// Here we look for authority to grant privilege to a app
|
|
|
|
// Currently, we don't revisit this decision, although in future we might
|
|
|
|
// return an array of privHT entries to which the new PrincipalT will be added
|
|
|
|
ArrayList arrarr = null;
|
|
|
|
eraseHistory = false;
|
|
|
|
if (appPrivs != null) {
|
|
|
|
for (int i = 0; i < appPrivs.Length; i++) {
|
|
|
|
string s = (!)appPrivs[i];
|
|
|
|
string acl = (string) authorityHT[s];
|
|
|
|
// Would call the normal CheckAccess entry point here, but it can't equate
|
|
|
|
// the Principal type in this context, and that in the library. Grumble!
|
|
|
|
if (acl != null && aclCore.CheckAccess(acl, null,
|
|
|
|
publisherT.val, publisherT.name)) {
|
|
|
|
ArrayList arr = (ArrayList) privHT[s];
|
|
|
|
if (arr == null) {
|
|
|
|
lock (privHT) {
|
|
|
|
arr = new ArrayList();
|
|
|
|
privHT[s] = arr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// make an array of ArrayLists which we'll add a principal handle to below
|
|
|
|
if (arrarr == null) {
|
|
|
|
arrarr = new ArrayList();
|
|
|
|
}
|
|
|
|
arrarr.Add(arr);
|
|
|
|
if (s == TruncateHistoryPrivilege) {
|
|
|
|
eraseHistory = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return arrarr;
|
|
|
|
}
|
|
|
|
|
2008-03-05 09:52:00 -05:00
|
|
|
private static void InstallPolicy(XmlNode! config)
|
|
|
|
{
|
|
|
|
// get all group definitions to register from the xml
|
|
|
|
XmlNode policyConfig = config.GetChild(AuthPolicyXmlTag);
|
|
|
|
|
|
|
|
if (policyConfig == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isAuthority = false;
|
|
|
|
foreach (XmlNode! elem in policyConfig.Children) {
|
|
|
|
string name;
|
|
|
|
string target;
|
|
|
|
if (elem.Name == AuthorityXmlTag) {
|
|
|
|
name = (!)elem.GetAttribute(AuthorityNameXmlAttribute, "");
|
|
|
|
target = (!)elem.GetAttribute(AuthorityAclXmlAttribute, "");
|
|
|
|
isAuthority = true;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
else if (elem.Name == SubexprXmlTag) {
|
2008-03-05 09:52:00 -05:00
|
|
|
name = (!)elem.GetAttribute(SubexprNameXmlAttribute, "");
|
|
|
|
target = (!)elem.GetAttribute(SubexprExpansionXmlAttribute, "");
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
else {
|
2008-03-05 09:52:00 -05:00
|
|
|
continue;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
if (name.Length != 0 && target.Length != 0) {
|
2008-11-17 18:29:00 -05:00
|
|
|
if (name.IndexOf('.') == -1) {
|
2008-03-05 09:52:00 -05:00
|
|
|
name = String.Format("{0}.{1}", name, DefaultNameContext);
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
if (isAuthority) {
|
2008-03-05 09:52:00 -05:00
|
|
|
authorityHT[name] = target;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
|
|
|
else {
|
2008-03-05 09:52:00 -05:00
|
|
|
exprHT[name] = target;
|
2008-11-17 18:29:00 -05:00
|
|
|
}
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static PrincipalT! GetPublisherT(string! publisher)
|
|
|
|
{
|
2008-11-17 18:29:00 -05:00
|
|
|
lock (publisherHT) {
|
|
|
|
PrincipalT publisherT = (PrincipalT) publisherHT[publisher];
|
|
|
|
if (publisherT == null) {
|
|
|
|
publisherT = new PrincipalT(publisher);
|
|
|
|
publisherHT[publisher] = publisherT;
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
2008-11-17 18:29:00 -05:00
|
|
|
return publisherT;
|
2008-03-05 09:52:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|