আপনি Apigee Edge-এর ডকুমেন্টেশন দেখছেন।
Apigee X ডকুমেন্টেশন .info- তে যান।
এই টপিকে Apigee Edge-এ কীভাবে OAuth 2.0 স্কোপ ব্যবহার করতে হয় তা আলোচনা করা হয়েছে।
OAuth2 স্কোপ কী?
OAuth 2.0 স্কোপ একটি অ্যাক্সেস টোকেনকে দেওয়া অ্যাক্সেসের পরিমাণ সীমিত করার একটি উপায় প্রদান করে। উদাহরণস্বরূপ, একটি ক্লায়েন্ট অ্যাপকে দেওয়া অ্যাক্সেস টোকেনকে সুরক্ষিত রিসোর্সগুলিতে READ এবং WRITE অ্যাক্সেস, অথবা শুধুমাত্র READ অ্যাক্সেস দেওয়া যেতে পারে। আপনি আপনার API-গুলিকে আপনার ইচ্ছামত যেকোনো স্কোপ বা স্কোপের সংমিশ্রণ প্রয়োগ করার জন্য ইমপ্লিমেন্ট করতে পারেন। সুতরাং, যদি কোনো ক্লায়েন্ট READ স্কোপযুক্ত একটি টোকেন পায় এবং এটি এমন একটি API এন্ডপয়েন্ট কল করার চেষ্টা করে যার জন্য WRITE অ্যাক্সেস প্রয়োজন, তাহলে কলটি ব্যর্থ হবে।
এই অংশে আমরা আলোচনা করব কিভাবে অ্যাক্সেস টোকেনে স্কোপ নির্ধারণ করা হয় এবং কিভাবে Apigee Edge, OAuth 2.0 স্কোপ প্রয়োগ করে। এই অংশটি পড়ার পর, আপনি আত্মবিশ্বাসের সাথে স্কোপ ব্যবহার করতে পারবেন।
অ্যাক্সেস টোকেনগুলিতে কীভাবে স্কোপ বরাদ্দ করা হয়?
Edge যখন একটি অ্যাক্সেস টোকেন তৈরি করে, তখন এটি সেই টোকেনটিতে একটি স্কোপ নির্ধারণ করে দিতে পারে। এটি কীভাবে ঘটে তা বোঝার জন্য, আপনাকে প্রথমে Apigee Edge-এর এই সত্তাগুলোর সাথে পরিচিত হতে হবে: API প্রোডাক্ট, ডেভেলপার এবং ডেভেলপার অ্যাপ। একটি পরিচিতির জন্য, ‘Introduction to publishing’ দেখুন। আমরা সুপারিশ করি যে, চালিয়ে যাওয়ার আগে আপনার প্রয়োজন হলে এই বিষয়বস্তু পর্যালোচনা করে নিন।
অ্যাক্সেস টোকেন হলো এলোমেলো দেখতে অক্ষরের একটি দীর্ঘ স্ট্রিং যা Edge-কে আগত API অনুরোধগুলি যাচাই করতে সাহায্য করে (এটিকে সাধারণ ইউজারনেম/পাসওয়ার্ড ক্রেডেনশিয়ালের একটি বিকল্প হিসেবে ভাবা যেতে পারে)। প্রযুক্তিগতভাবে, টোকেনটি একটি কী যা মেটাডেটার একটি সংগ্রহকে নির্দেশ করে, যা দেখতে এইরকম:
{ "issued_at" : "1416962591727", "application_name" : "0d3e1d41-a59f-4d74-957e-d4e3275d4781", "scope" : "A", "status" : "approved", "api_product_list" : "[scopecheck1-bs0cSuqS9y]", "expires_in" : "1799", //--in seconds "developer.email" : "scopecheck1-AdBmANhsag@apigee.com", "organization_id" : "0", "token_type" : "BearerToken", "client_id" : "eTtB7w5lvk3DnOZNGReBlvGvIAeAywun", "access_token" : "ODm47ris5AlEty8TDc1itwYPe5MW", "organization_name" : "wwitman", "refresh_token_expires_in" : "0", //--in seconds "refresh_count" : "0" }
টোকেনটির মেটাডেটাতে প্রকৃত অ্যাক্সেস টোকেন স্ট্রিং, মেয়াদ শেষ হওয়ার তথ্য, ডেভেলপার অ্যাপ ও ডেভেলপারের শনাক্তকরণ এবং টোকেনটির সাথে যুক্ত প্রোডাক্টগুলোর বিবরণ অন্তর্ভুক্ত থাকে। আপনি আরও লক্ষ্য করবেন যে, মেটাডেটাতে 'স্কোপ' (scope) অংশটিও অন্তর্ভুক্ত থাকে।
টোকেনটি কীভাবে তার স্কোপ পায়?
স্কোপ বোঝার প্রথম চাবিকাঠি হলো এটা মনে রাখা যে, একটি ডেভেলপার অ্যাপের প্রতিটি প্রোডাক্টের সাথে শূন্য বা তার বেশি স্কোপ যুক্ত থাকতে পারে। এই স্কোপগুলো প্রোডাক্ট তৈরির সময় নির্ধারণ করা যেতে পারে, অথবা পরেও যোগ করা যেতে পারে। এগুলো নামের একটি তালিকা হিসেবে থাকে এবং প্রতিটি প্রোডাক্টের সাথে যুক্ত 'মেটাডেটা'-তে অন্তর্ভুক্ত থাকে।
যখন আপনি একটি ডেভেলপার অ্যাপ তৈরি করেন এবং তাতে প্রোডাক্ট যোগ করেন, তখন Edge সেই অ্যাপের সমস্ত প্রোডাক্ট দেখে এবং সেগুলোর জন্য সমস্ত স্কোপের একটি তালিকা তৈরি করে (অ্যাপটির মাস্টার বা গ্লোবাল স্কোপ তালিকা — যা সমস্ত স্বীকৃত স্কোপের একটি সমষ্টি)।
যখন কোনো ক্লায়েন্ট অ্যাপ Apigee Edge থেকে একটি অ্যাক্সেস টোকেনের জন্য অনুরোধ করে, তখন এটি ঐচ্ছিকভাবে নির্দিষ্ট করে দিতে পারে যে সেই টোকেনের সাথে কোন স্কোপগুলো যুক্ত করতে চায়। উদাহরণস্বরূপ, নিম্নলিখিত অনুরোধটি "A" স্কোপের জন্য অনুরোধ করছে। অর্থাৎ, ক্লায়েন্টটি অথরাইজেশন সার্ভারকে (Edge) "A" স্কোপের একটি অ্যাক্সেস টোকেন তৈরি করতে বলছে (যা অ্যাপটিকে "A" স্কোপের API কল করার অনুমোদন দেবে)। অ্যাপটি এইরকম একটি POST অনুরোধ পাঠায়:
curl -i -X POST -H Authorization: Basic Mg12YTk2UkEIyIBCrtro1QpIG -H content-type:application/x-www-form-urlencoded http://myorg-test.apigee.net/oauth/token?grant_type=client_credentials&scope=A
কী ঘটে?
Edge যখন এই অনুরোধটি গ্রহণ করে, তখন এটি জানতে পারে কোন অ্যাপ অনুরোধটি করছে এবং ক্লায়েন্ট কোন ডেভেলপার অ্যাপে নিবন্ধন করেছে (ক্লায়েন্ট আইডি এবং ক্লায়েন্ট সিক্রেট কী বেসিক অথ হেডারে এনকোড করা থাকে)। যেহেতু scope কোয়েরি প্যারামিটারটি অন্তর্ভুক্ত থাকে, তাই Edge-কে সিদ্ধান্ত নিতে হয় যে ডেভেলপার অ্যাপটির সাথে যুক্ত কোনো API প্রোডাক্টের স্কোপ "A" আছে কি না। যদি থাকে, তাহলে স্কোপ "A" সহ একটি অ্যাক্সেস টোকেন তৈরি করা হয়। এটিকে অন্যভাবেও দেখা যেতে পারে যে, স্কোপ কোয়েরি প্যারামিটারটি এক ধরনের ফিল্টার। যদি ডেভেলপার অ্যাপটি "A, B, X" স্কোপগুলো শনাক্ত করতে পারে এবং কোয়েরি প্যারামিটারে "scope=XYZ" উল্লেখ করা থাকে, তাহলে টোকেনটিতে শুধুমাত্র "X" স্কোপটিই বরাদ্দ করা হবে।
যদি ক্লায়েন্ট কোনো স্কোপ প্যারামিটার সংযুক্ত না করে, তাহলে কী হবে? এই ক্ষেত্রে, Edge এমন একটি টোকেন তৈরি করে যাতে ডেভেলপার অ্যাপ দ্বারা স্বীকৃত সমস্ত স্কোপ অন্তর্ভুক্ত থাকে। এটা বোঝা গুরুত্বপূর্ণ যে, ডিফল্ট আচরণ হলো এমন একটি অ্যাক্সেস টোকেন ফেরত দেওয়া, যাতে ডেভেলপার অ্যাপে অন্তর্ভুক্ত সমস্ত প্রোডাক্টের সব স্কোপের সমষ্টি থাকে।
যদি কোনো ডেভেলপার অ্যাপের সাথে যুক্ত কোনো প্রোডাক্টে স্কোপ নির্দিষ্ট করা না থাকে, এবং কোনো টোকেনের স্কোপ থাকে, তাহলে সেই টোকেন দিয়ে করা কলগুলো ব্যর্থ হবে।
ধরা যাক, একটি ডেভেলপার অ্যাপ এই স্কোপগুলো শনাক্ত করে: ABC D। এটি হলো অ্যাপটির স্কোপগুলোর মূল তালিকা। এমন হতে পারে যে, অ্যাপটির একটি পণ্যের স্কোপ A ও B, এবং দ্বিতীয়টির স্কোপ C ও D, অথবা এগুলোর যেকোনো সংমিশ্রণ। যদি ক্লায়েন্ট কোনো scope প্যারামিটার নির্দিষ্ট না করে (অথবা যদি এটি কোনো মান ছাড়া স্কোপ প্যারামিটার নির্দিষ্ট করে), তাহলে টোকেনটিকে চারটি স্কোপই প্রদান করা হবে: A, B, C, এবং D। আবার, টোকেনটি এমন একটি স্কোপ সেট পায় যা হলো ডেভেলপার অ্যাপ দ্বারা শনাক্তকৃত সমস্ত স্কোপের সমষ্টি।
আরও একটি ক্ষেত্র আছে যেখানে ডিফল্ট আচরণ হলো সমস্ত স্বীকৃত স্কোপ সহ একটি অ্যাক্সেস টোকেন ফেরত দেওয়া, এবং সেটি হলো যখন GenerateAccessToken পলিসি (Apigee Edge-এর যে পলিসিটি অ্যাক্সেস টোকেন তৈরি করে) একটি <Scope> এলিমেন্ট নির্দিষ্ট করে না । উদাহরণস্বরূপ, এখানে একটি GenerateAccessToken পলিসি দেওয়া হলো যেখানে <Scope> নির্দিষ্ট করা আছে । যদি সেই <Scope> এলিমেন্টটি অনুপস্থিত থাকে (অথবা যদি এটি উপস্থিত থেকেও খালি থাকে), তাহলে ডিফল্ট আচরণটি কার্যকর হয়।
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-GenerateAccessToken"> <DisplayName>OAuthV2 - Generate Access Token</DisplayName> <Attributes> <Attribute name='hello' ref='system.time' display='false'>value1</Attribute> </Attributes> <Scope>request.queryparam.scope</Scope> <GrantType>request.formparam.grant_type</GrantType> <ExternalAuthorization>false</ExternalAuthorization> <Operation>GenerateAccessToken</Operation> <SupportedGrantTypes> <GrantType>client_credentials</GrantType> </SupportedGrantTypes> <GenerateResponse enabled="true"/> </OAuthV2>
স্কোপগুলো কীভাবে প্রয়োগ করা হয়?
প্রথমত, মনে রাখবেন যে Apigee Edge-এ অ্যাক্সেস টোকেনগুলো OAuthV2 পলিসির মাধ্যমে যাচাই করা হয় (যা সাধারণত একটি প্রক্সি ফ্লো-এর একেবারে শুরুতে রাখা হয়)। পলিসিটিতে অবশ্যই VerifyAccessToken অপারেশনটি নির্দিষ্ট করা থাকতে হবে। চলুন এই পলিসিটি দেখা যাক:
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenA">
<DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
<ExternalAuthorization>false</ExternalAuthorization>
<Operation>VerifyAccessToken</Operation>
<Scope>A</Scope> <!-- Optional: space-separated list of scope names. -->
<GenerateResponse enabled="true"/>
</OAuthV2> <Scope> এলিমেন্টটি লক্ষ্য করুন। পলিসিটি কোন কোন স্কোপ গ্রহণ করবে, তা নির্দিষ্ট করতে এটি ব্যবহৃত হয়।
এই উদাহরণে, পলিসিটি তখনই সফল হবে যখন অ্যাক্সেস টোকেনে "A" স্কোপ অন্তর্ভুক্ত থাকবে। যদি এই <Scope> এলিমেন্টটি বাদ দেওয়া হয় বা এর কোনো ভ্যালু না থাকে, তাহলে পলিসিটি অ্যাক্সেস টোকেনের স্কোপকে উপেক্ষা করবে।
এখন, স্কোপের উপর ভিত্তি করে অ্যাক্সেস টোকেন যাচাই করার ক্ষমতার সাহায্যে, আপনি নির্দিষ্ট স্কোপ প্রয়োগ করার জন্য আপনার API ডিজাইন করতে পারেন। এটি করার জন্য, আপনাকে স্কোপ-সচেতন VerifyAccessToken পলিসি সংযুক্ত করে কাস্টম ফ্লো ডিজাইন করতে হবে।
ধরা যাক, আপনার API-তে /resourceA এন্ডপয়েন্টের জন্য একটি ফ্লো সংজ্ঞায়িত করা আছে:
<Flow name="resourceA">
<Condition>(proxy.pathsuffix MatchesPath "/resourceA") and (request.verb = "GET")</Condition>
<Description>Get a resource A</Description>
<Request>
<Step>
<Name>OAuthV2-VerifyAccessTokenA</Name>
</Step>
</Request>
<Response>
<Step>
<Name>AssignMessage-CreateResponse</Name>
</Step>
</Response>
</Flow> যখন এই ফ্লোটি ট্রিগার হয় (অর্থাৎ পাথ সাফিক্সে /resourceA সহ কোনো রিকোয়েস্ট আসে), তখন OAuthV2-VerifyAccessTokenA পলিসিটি সাথে সাথে কল করা হয়। এই পলিসিটি যাচাই করে যে অ্যাক্সেস টোকেনটি বৈধ কিনা এবং টোকেনটি কোন কোন স্কোপ সাপোর্ট করে তা দেখে নেয়। যদি পলিসিটি নিচের উদাহরণের মতো <Scope>A</Scope> দিয়ে কনফিগার করা থাকে, তবে পলিসিটি কেবল তখনই সফল হবে যদি অ্যাক্সেস টোকেনটির স্কোপ "A" হয়। অন্যথায়, এটি একটি এরর রিটার্ন করবে।
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenA">
<DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
<ExternalAuthorization>false</ExternalAuthorization>
<Operation>VerifyAccessToken</Operation>
<Scope>A</Scope>
<GenerateResponse enabled="true"/>
</OAuthV2>সংক্ষেপে বলতে গেলে, এপিআই ডেভেলপাররা তাদের এপিআই-তে স্কোপ এনফোর্সমেন্ট ডিজাইন করার জন্য দায়ী। তারা নির্দিষ্ট স্কোপগুলো পরিচালনা করার জন্য কাস্টম ফ্লো তৈরি করে এবং সেই স্কোপগুলো প্রয়োগ করতে VerifyAccessToken পলিসি সংযুক্ত করে এই কাজটি করে থাকেন।
কোডের উদাহরণ
সবশেষে, টোকেনগুলো কীভাবে স্কোপ গ্রহণ করে এবং কীভাবে স্কোপগুলো প্রয়োগ করা হয়, তা বোঝানোর জন্য চলুন কিছু উদাহরণ এপিআই কল দেখে নেওয়া যাক।
ডিফল্ট কেস
ধরা যাক, আপনার একটি ডেভেলপার অ্যাপ আছে যেখানে কিছু প্রোডাক্ট রয়েছে এবং সেই প্রোডাক্টগুলোর স্কোপের সমষ্টি হলো: A, B, এবং C। এই এপিআই কলটি একটি অ্যাক্সেস টোকেনের জন্য অনুরোধ করে, কিন্তু কোনো স্কোপ কোয়েরি প্যারামিটার নির্দিষ্ট করে না।
curl -X POST -H content-type:application/x-www-form-urlencoded http://wwitman-test.apigee.net/scopecheck1/token?grant_type=client_credentials
এক্ষেত্রে, তৈরি হওয়া টোকেনটিকে A, B, এবং C স্কোপগুলো দেওয়া হবে (এটিই ডিফল্ট আচরণ)। টোকেনটির মেটাডেটা দেখতে অনেকটা এইরকম হবে:
{ "issued_at" : "1417016208588", "application_name" : "eb1a0333-5775-4116-9eb2-c36075ddc360", "scope" : "A B C", "status" : "approved", "api_product_list" : "[scopecheck1-yEgQbQqjRR]", "expires_in" : "1799", //--in seconds "developer.email" : "scopecheck1-yxiuHuZcDW@apigee.com", "organization_id" : "0", "token_type" : "BearerToken", "client_id" : "atGFvl3jgA0pJd05rXKHeNAC69naDmpW", "access_token" : "MveXpj4UYXol38thNoJYIa8fBGlI", "organization_name" : "wwitman", "refresh_token_expires_in" : "0", //--in seconds "refresh_count" : "0" }
এখন, ধরা যাক আপনার একটি API এন্ডপয়েন্ট আছে যার স্কোপ 'A' (অর্থাৎ, এর VerifyAccessToken-এর জন্য স্কোপ 'A' প্রয়োজন)। VerifyAccessToken পলিসিটি নিচে দেওয়া হলো:
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenA">
<DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
<ExternalAuthorization>false</ExternalAuthorization>
<Operation>VerifyAccessToken</Operation>
<Scope>A</Scope>
<GenerateResponse enabled="true"/>
</OAuthV2>এখানে একটি এন্ডপয়েন্টে কলের নমুনা দেওয়া হলো যা স্কোপ A প্রয়োগ করে:
curl -X GET -H Authorization: Bearer MveXpj4UYXol38thNoJYIa8fBGlI http://wwitman-test.apigee.net/scopecheck1/resourceA
এই GET কলটি সফল হয়েছে:
{
"hello" : "Tue, 25 Nov 2014 01:35:53 UTC"
}এটি সফল হয় কারণ এন্ডপয়েন্টটি কল করার সময় যে VerifyAccessToken পলিসিটি সক্রিয় হয়, সেটির জন্য স্কোপ A প্রয়োজন, এবং অ্যাক্সেস টোকেনটিকে স্কোপ A, B, ও C প্রদান করা হয়েছিল—যা এর ডিফল্ট আচরণ।
ফিল্টারিং কেস
ধরা যাক, আপনার একটি ডেভেলপার অ্যাপ আছে যার প্রোডাক্টগুলোর স্কোপ হলো A, B, C, এবং X। আপনি একটি অ্যাক্সেস টোকেনের জন্য অনুরোধ করেন এবং scope কোয়েরি প্যারামিটারটি এভাবে অন্তর্ভুক্ত করেন:
curl -i -X POST -H content-type:application/x-www-form-urlencoded 'http://myorg-test.apigee.net/oauth/token?grant_type=client_credentials&scope=A X'
এই ক্ষেত্রে, তৈরি হওয়া টোকেনটিকে A এবং X স্কোপ দেওয়া হবে, কারণ A এবং X উভয়ই বৈধ স্কোপ। মনে রাখবেন যে ডেভেলপার অ্যাপটি A, B, C, এবং X স্কোপগুলো শনাক্ত করে। এই ক্ষেত্রে, আপনি এই স্কোপগুলোর উপর ভিত্তি করে API প্রোডাক্টের তালিকা ফিল্টার করছেন। যদি কোনো প্রোডাক্টের স্কোপ A বা X থাকে, তাহলে আপনি এমন API এন্ডপয়েন্ট কনফিগার করতে পারেন যা এই স্কোপগুলো প্রয়োগ করবে। যদি কোনো প্রোডাক্টের স্কোপ A বা X না থাকে (ধরা যাক এর স্কোপ B, C, এবং Z), তাহলে যে API-গুলো A বা X স্কোপ প্রয়োগ করে, সেগুলোকে টোকেন দিয়ে কল করা যাবে না।
যখন আপনি নতুন টোকেন দিয়ে এপিআই কল করবেন:
curl -X GET -H Authorization: Bearer Rkmqo2UkEIyIBCrtro1QpIG http://wwitman-test.apigee.net/scopecheck1/resourceX
অ্যাক্সেস টোকেনটি এপিআই প্রক্সি দ্বারা যাচাই করা হয়। উদাহরণস্বরূপ:
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenX">
<DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
<ExternalAuthorization>false</ExternalAuthorization>
<Operation>VerifyAccessToken</Operation>
<Scope>A X</Scope>
<GenerateResponse enabled="true"/>
</OAuthV2>GET কলটি সফলভাবে ট্রিগার হয় এবং এটি একটি প্রতিক্রিয়া ফেরত দেয়। উদাহরণস্বরূপ:
{
"hello" : "Tue, 25 Nov 2014 01:35:53 UTC"
}
এটি সফল হয় কারণ VerifyAccessToken পলিসির জন্য স্কোপ A বা X প্রয়োজন, এবং অ্যাক্সেস টোকেনটিতে স্কোপ A ও X অন্তর্ভুক্ত রয়েছে। অবশ্যই, যদি <Scope> এলিমেন্টটি "B"-তে সেট করা থাকত, তাহলে এই কলটি ব্যর্থ হতো।
সারসংক্ষেপ
Apigee Edge কীভাবে OAuth 2.0 স্কোপগুলো পরিচালনা করে, তা বোঝা জরুরি। নিচে কিছু গুরুত্বপূর্ণ বিষয় তুলে ধরা হলো:
- একটি ডেভেলপার অ্যাপ তার সমস্ত পণ্যের জন্য সংজ্ঞায়িত সকল স্কোপের সমষ্টিকে 'স্বীকৃতি' দেয়।
- যখন কোনো অ্যাপ অ্যাক্সেস টোকেনের জন্য অনুরোধ করে, তখন এটি কোন স্কোপগুলো পেতে চায় তা নির্দিষ্ট করার সুযোগ পায়। (ক) অনুরোধ করা স্কোপ(গুলো) এবং (খ) ডেভেলপার অ্যাপ দ্বারা স্বীকৃত স্কোপগুলোর উপর ভিত্তি করে, অ্যাক্সেস টোকেনটিতে প্রকৃতপক্ষে কোন স্কোপগুলো বরাদ্দ করা হবে, তা নির্ধারণ করার দায়িত্ব Apigee Edge (অথরাইজেশন সার্ভার)-এর।
- যদি Apigee Edge স্কোপ যাচাই করার জন্য কনফিগার করা না থাকে (অর্থাৎ VerifyAccessToken পলিসিতে
<Scope>এলিমেন্টটি অনুপস্থিত থাকে বা এটি খালি থাকে), তাহলে API কলটি সফল হবে, যদি অ্যাক্সেস টোকেনে এমবেড করা স্কোপটি নিবন্ধিত ডেভেলপার অ্যাপ দ্বারা স্বীকৃত স্কোপগুলোর (অ্যাপটির "মাস্টার" স্কোপ তালিকার) কোনো একটির সাথে মিলে যায়। - যদি কোনো অ্যাক্সেস টোকেনের সাথে কোনো স্কোপ যুক্ত না থাকে, তাহলে এটি কেবল তখনই সফল হবে যখন Edge স্কোপ বিবেচনা করে না (অর্থাৎ VerifyAccessToken পলিসিতে
<Scope>এলিমেন্টটি অনুপস্থিত বা এটি খালি থাকে)।