[AAF-21] Updated Copyright Headers for AAF
[aaf/authz.git] / authz-certman / src / main / java / com / att / authz / cm / service / CMService.java
1 /*******************************************************************************\r
2  * ============LICENSE_START====================================================\r
3  * * org.onap.aaf\r
4  * * ===========================================================================\r
5  * * Copyright © 2017 AT&T Intellectual Property. All rights reserved.\r
6  * * ===========================================================================\r
7  * * Licensed under the Apache License, Version 2.0 (the "License");\r
8  * * you may not use this file except in compliance with the License.\r
9  * * You may obtain a copy of the License at\r
10  * * \r
11  *  *      http://www.apache.org/licenses/LICENSE-2.0\r
12  * * \r
13  *  * Unless required by applicable law or agreed to in writing, software\r
14  * * distributed under the License is distributed on an "AS IS" BASIS,\r
15  * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
16  * * See the License for the specific language governing permissions and\r
17  * * limitations under the License.\r
18  * * ============LICENSE_END====================================================\r
19  * *\r
20  * * ECOMP is a trademark and service mark of AT&T Intellectual Property.\r
21  * *\r
22  ******************************************************************************/\r
23 package com.att.authz.cm.service;\r
24 \r
25 import java.io.IOException;\r
26 import java.net.InetAddress;\r
27 import java.net.UnknownHostException;\r
28 import java.nio.ByteBuffer;\r
29 import java.security.NoSuchAlgorithmException;\r
30 import java.security.cert.X509Certificate;\r
31 import java.util.ArrayList;\r
32 import java.util.Date;\r
33 import java.util.List;\r
34 \r
35 import com.att.authz.cm.api.API_Cert;\r
36 import com.att.authz.cm.ca.CA;\r
37 import com.att.authz.cm.cert.BCFactory;\r
38 import com.att.authz.cm.cert.CSRMeta;\r
39 import com.att.authz.cm.data.CertDrop;\r
40 import com.att.authz.cm.data.CertRenew;\r
41 import com.att.authz.cm.data.CertReq;\r
42 import com.att.authz.cm.data.CertResp;\r
43 import com.att.authz.cm.validation.Validator;\r
44 import com.att.authz.env.AuthzTrans;\r
45 import com.att.authz.layer.Result;\r
46 import com.att.authz.org.Organization;\r
47 import com.att.authz.org.Organization.Identity;\r
48 import com.att.authz.org.OrganizationException;\r
49 import com.att.cadi.Hash;\r
50 import com.att.cadi.aaf.AAFPermission;\r
51 import com.att.cadi.aaf.v2_0.AAFCon;\r
52 import com.att.cadi.cm.Factory;\r
53 import com.att.dao.CassAccess;\r
54 import com.att.dao.DAO;\r
55 import com.att.dao.aaf.cass.ArtiDAO;\r
56 import com.att.dao.aaf.cass.CacheInfoDAO;\r
57 import com.att.dao.aaf.cass.CertDAO;\r
58 import com.att.dao.aaf.cass.CredDAO;\r
59 import com.att.dao.aaf.cass.HistoryDAO;\r
60 import com.att.dao.aaf.cass.Status;\r
61 import com.att.dao.aaf.hl.Question;\r
62 import com.att.inno.env.APIException;\r
63 import com.att.inno.env.Slot;\r
64 import com.att.inno.env.util.Chrono;\r
65 import com.datastax.driver.core.Cluster;\r
66 \r
67 \r
68 public class CMService {\r
69         // If we add more CAs, may want to parameterize\r
70         private static final int STD_RENEWAL = 30;\r
71         private static final int MAX_RENEWAL = 60;\r
72         private static final int MIN_RENEWAL = 10;\r
73         \r
74         public static final String REQUEST = "request";\r
75         public static final String RENEW = "renew";\r
76         public static final String DROP = "drop";\r
77         public static final String SANS = "san";\r
78         \r
79         private static final String[] NO_NOTES = new String[0];\r
80         private Slot sCertAuth;\r
81         private final CertDAO certDAO;\r
82         private final CredDAO credDAO;\r
83         private final ArtiDAO artiDAO;\r
84         private DAO<AuthzTrans, ?>[] daos;\r
85 \r
86         @SuppressWarnings("unchecked")\r
87         public CMService(AuthzTrans trans, CertManAPI certman) throws APIException, IOException {\r
88 \r
89                 sCertAuth = certman.env.slot(API_Cert.CERT_AUTH);\r
90                 Cluster cluster;\r
91                 try {\r
92                         cluster = com.att.dao.CassAccess.cluster(certman.env,null);\r
93                 } catch (IOException e) {\r
94                         throw new APIException(e);\r
95                 }\r
96 \r
97                 // jg 4/2015 SessionFilter unneeded... DataStax already deals with Multithreading well\r
98                 \r
99                 HistoryDAO hd = new HistoryDAO(trans,  cluster, CassAccess.KEYSPACE);\r
100                 CacheInfoDAO cid = new CacheInfoDAO(trans, hd);\r
101                 certDAO = new CertDAO(trans, hd, cid);\r
102                 credDAO = new CredDAO(trans, hd, cid);\r
103                 artiDAO = new ArtiDAO(trans, hd, cid);\r
104                 \r
105                 daos =(DAO<AuthzTrans, ?>[]) new DAO<?,?>[] {\r
106                                 hd,cid,certDAO,credDAO,artiDAO\r
107                 };\r
108 \r
109                 // Setup Shutdown Hooks for Cluster and Pooled Sessions\r
110                 Runtime.getRuntime().addShutdownHook(new Thread() {\r
111                         @Override\r
112                         public void run() {\r
113                                 for(DAO<AuthzTrans,?> dao : daos) {\r
114                                         dao.close(trans);\r
115                                 }\r
116 \r
117 //                              sessionFilter.destroy();\r
118                                 cluster.close();\r
119                         }\r
120                 }); \r
121         }\r
122         \r
123         public Result<CertResp> requestCert(AuthzTrans trans,Result<CertReq> req) {\r
124                 if(req.isOK()) {\r
125                         CA ca = trans.get(sCertAuth, null);\r
126                         if(ca==null) {\r
127                                 return Result.err(Result.err(Result.ERR_BadData, "Invalid Cert Authority requested"));\r
128                         }\r
129 \r
130                         // Allow only AAF CA without special permission\r
131                         if(!ca.getName().equals("aaf") && !trans.fish( new AAFPermission(ca.getPermType(), ca.getName(), REQUEST))) {\r
132                                 return Result.err(Status.ERR_Denied, "'%s' does not have permission to request Certificates from Certificate Authority '%s'", \r
133                                                 trans.user(),ca.getName());\r
134                         }\r
135 \r
136                         List<String> notes = null;\r
137                         List<String> fqdns;\r
138                         String email = null;\r
139 \r
140                         try {\r
141                                 Organization org = trans.org();\r
142                                 \r
143                                 // Policy 1: Requests are only by Pre-Authorized Configurations\r
144                                 ArtiDAO.Data add = null;\r
145                                 try {\r
146                                         for(InetAddress ia : InetAddress.getAllByName(trans.ip())) {\r
147                                                 Result<List<ArtiDAO.Data>> ra = artiDAO.read(trans, req.value.mechid,ia.getHostName());\r
148                                                 if(ra.isOKhasData()) {\r
149                                                         add = ra.value.get(0);\r
150                                                         break;\r
151                                                 }\r
152                                         }\r
153                                 } catch (UnknownHostException e1) {\r
154                                         return Result.err(Result.ERR_BadData,"There is no host for %s",trans.ip());\r
155                                 }\r
156                                 \r
157                                 if(add==null) {\r
158                                         return Result.err(Result.ERR_BadData,"There is no configuration for %s",req.value.mechid);\r
159                                 }\r
160                                 \r
161                                 // Policy 2: If Config marked as Expired, do not create or renew\r
162                                 Date now = new Date();\r
163                                 if(add.expires!=null && now.after(add.expires)) {\r
164                                         return Result.err(Result.ERR_Policy,"Configuration for %s %s is expired %s",add.mechid,add.machine,Chrono.dateFmt.format(add.expires));\r
165                                 }\r
166                                 \r
167                                 // Policy 3: MechID must be current\r
168                                 Identity muser = org.getIdentity(trans, add.mechid);\r
169                                 if(muser == null) {\r
170                                         return Result.err(Result.ERR_Policy,"MechID must exist in %s",org.getName());\r
171                                 }\r
172                                 \r
173                                 // Policy 4: Sponsor must be current\r
174                                 Identity ouser = muser.owner();\r
175                                 if(ouser==null) {\r
176                                         return Result.err(Result.ERR_Policy,"%s does not have a current sponsor at %s",add.mechid,org.getName());\r
177                                 } else if(!ouser.isFound() || !ouser.isResponsible()) {\r
178                                         return Result.err(Result.ERR_Policy,"%s reports that %s cannot be responsible for %s",org.getName(),trans.user());\r
179                                 }\r
180                                 \r
181                                         // Set Email from most current Sponsor\r
182                                 email = ouser.email();\r
183                                 \r
184                                 // Policy 5: keep Artifact data current\r
185                                 if(!ouser.fullID().equals(add.sponsor)) {\r
186                                         add.sponsor = ouser.fullID();\r
187                                         artiDAO.update(trans, add);\r
188                                 }\r
189                 \r
190                                 // Policy 6: Requester must be granted Change permission in Namespace requested\r
191                                 String mechNS = AAFCon.reverseDomain(req.value.mechid);\r
192                                 if(mechNS==null) {\r
193                                         return Result.err(Status.ERR_Denied, "%s does not reflect a valid AAF Namespace",req.value.mechid);\r
194                                 }\r
195                                 \r
196                                 // Policy 7: Caller must be the MechID or have specifically delegated permissions\r
197                                 if(!trans.user().equals(req.value.mechid) && !trans.fish(new AAFPermission(mechNS + ".certman", ca.getName() , "request"))) {\r
198                                         return Result.err(Status.ERR_Denied, "%s must have access to modify x509 certs in NS %s",trans.user(),mechNS);\r
199                                 }\r
200                                 \r
201         \r
202                                 // Policy 8: SANs only allowed by Exception... need permission\r
203                                 fqdns = new ArrayList<String>();\r
204                                 fqdns.add(add.machine);  // machine is first\r
205                                 if(req.value.fqdns.size()>1 && !trans.fish(new AAFPermission(ca.getPermType(), ca.getName(), SANS))) {\r
206                                         if(notes==null) {notes = new ArrayList<String>();}\r
207                                         notes.add("Warning: Subject Alternative Names only allowed by Permission: Get CSO Exception.  This Certificate will be created, but without SANs");\r
208                                 } else {\r
209                                         for(String m : req.value.fqdns) {\r
210                                                 if(!add.machine.equals(m)) {\r
211                                                         fqdns.add(m);\r
212                                                 }\r
213                                         }\r
214                                 }\r
215                                 \r
216                         } catch (Exception e) {\r
217                                 trans.error().log(e);\r
218                                 return Result.err(Status.ERR_Denied,"MechID Sponsorship cannot be determined at this time.  Try later");\r
219                         }\r
220                         \r
221                         CSRMeta csrMeta;\r
222                         try {\r
223                                 csrMeta = BCFactory.createCSRMeta(\r
224                                                 ca, \r
225                                                 req.value.mechid, \r
226                                                 email, \r
227                                                 fqdns);\r
228                                 X509Certificate x509 = ca.sign(trans, csrMeta);\r
229                                 if(x509==null) {\r
230                                         return Result.err(Result.ERR_ActionNotCompleted,"x509 Certificate not signed by CA");\r
231                                 }\r
232                                 CertDAO.Data cdd = new CertDAO.Data();\r
233                                 cdd.ca=ca.getName();\r
234                                 cdd.serial=x509.getSerialNumber();\r
235                                 cdd.id=req.value.mechid;\r
236                                 cdd.x500=x509.getSubjectDN().getName();\r
237                                 cdd.x509=Factory.toString(trans, x509);\r
238                                 certDAO.create(trans, cdd);\r
239                                 \r
240                                 CredDAO.Data crdd = new CredDAO.Data();\r
241                                 crdd.other = Question.random.nextInt();\r
242                                 crdd.cred=getChallenge256SaltedHash(csrMeta.challenge(),crdd.other);\r
243                                 crdd.expires = x509.getNotAfter();\r
244                                 crdd.id = req.value.mechid;\r
245                                 crdd.ns = Question.domain2ns(crdd.id);\r
246                                 crdd.type = CredDAO.CERT_SHA256_RSA;\r
247                                 credDAO.create(trans, crdd);\r
248                                 \r
249                                 CertResp cr = new CertResp(trans,x509,csrMeta, compileNotes(notes));\r
250                                 return Result.ok(cr);\r
251                         } catch (Exception e) {\r
252                                 trans.error().log(e);\r
253                                 return Result.err(Result.ERR_ActionNotCompleted,e.getMessage());\r
254                         }\r
255                 } else {\r
256                         return Result.err(req);\r
257                 }\r
258         }\r
259 \r
260     public Result<CertResp> renewCert(AuthzTrans trans, Result<CertRenew> renew) {\r
261                 if(renew.isOK()) {\r
262                         return Result.err(Result.ERR_NotImplemented,"Not implemented yet");\r
263                 } else {\r
264                         return Result.err(renew);\r
265                 }       \r
266         }\r
267 \r
268         public Result<Void> dropCert(AuthzTrans trans, Result<CertDrop> drop) {\r
269                 if(drop.isOK()) {\r
270                         return Result.err(Result.ERR_NotImplemented,"Not implemented yet");\r
271                 } else {\r
272                         return Result.err(drop);\r
273                 }       \r
274         }\r
275 \r
276         ///////////////\r
277         // Artifact\r
278         //////////////\r
279         public Result<Void> createArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {\r
280                 Validator v = new Validator().artisRequired(list, 1);\r
281                 if(v.err()) {\r
282                         return Result.err(Result.ERR_BadData,v.errs());\r
283                 }\r
284                 for(ArtiDAO.Data add : list) {\r
285                         try {\r
286                                 // Policy 1: MechID must exist in Org\r
287                                 Identity muser = trans.org().getIdentity(trans, add.mechid);\r
288                                 if(muser == null) {\r
289                                         return Result.err(Result.ERR_Denied,"%s is not valid for %s", add.mechid,trans.org().getName());\r
290                                 }\r
291                                 \r
292                                 // Policy 2: MechID must have valid Organization Owner\r
293                                 Identity ouser = muser.owner();\r
294                                 if(ouser == null) {\r
295                                         return Result.err(Result.ERR_Denied,"%s is not a valid Sponsor for %s at %s",\r
296                                                         trans.user(),add.mechid,trans.org().getName());\r
297                                 }\r
298                                 \r
299                                 // Policy 3: Calling ID must be MechID Owner\r
300                                 if(!trans.user().equals(ouser.fullID())) {\r
301                                         return Result.err(Result.ERR_Denied,"%s is not the Sponsor for %s at %s",\r
302                                                         trans.user(),add.mechid,trans.org().getName());\r
303                                 }\r
304 \r
305                                 // Policy 4: Renewal Days are between 10 and 60 (constants, may be parameterized)\r
306                                 if(add.renewDays<MIN_RENEWAL) {\r
307                                         add.renewDays = STD_RENEWAL;\r
308                                 } else if(add.renewDays>MAX_RENEWAL) {\r
309                                         add.renewDays = MAX_RENEWAL;\r
310                                 }\r
311                                 \r
312                                 // Policy 5: If Notify is blank, set to Owner's Email\r
313                                 if(add.notify==null || add.notify.length()==0) {\r
314                                         add.notify = "mailto:"+ouser.email();\r
315                                 }\r
316 \r
317                                 // Set Sponsor from Golden Source\r
318                                 add.sponsor = ouser.fullID();\r
319                                 \r
320                                 \r
321                         } catch (OrganizationException e) {\r
322                                 return Result.err(e);\r
323                         }\r
324                         // Add to DB\r
325                         Result<ArtiDAO.Data> rv = artiDAO.create(trans, add);\r
326                         // TODO come up with Partial Reporting Scheme, or allow only one at a time.\r
327                         if(rv.notOK()) {\r
328                                 return Result.err(rv);\r
329                         }\r
330                 }\r
331                 return Result.ok();\r
332         }\r
333 \r
334         public Result<List<ArtiDAO.Data>> readArtifacts(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {\r
335                 Validator v = new Validator().keys(add);\r
336                 if(v.err()) {\r
337                         return Result.err(Result.ERR_BadData,v.errs());\r
338                 }\r
339                 String ns = AAFCon.reverseDomain(add.mechid);\r
340                 \r
341                 if( trans.user().equals(add.mechid)\r
342                         || trans.fish(new AAFPermission(ns + ".access", "*", "read"))\r
343                         || (trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,add.mechid))==null) {\r
344                                 return artiDAO.read(trans, add);\r
345                 } else {\r
346                         return Result.err(Result.ERR_Denied,"%s is not %s, is not the sponsor, and doesn't have delegated permission.",trans.user(),add.mechid); // note: reason is set by 2nd case, if 1st case misses\r
347                 }\r
348 \r
349         }\r
350 \r
351         public Result<List<ArtiDAO.Data>> readArtifactsByMechID(AuthzTrans trans, String mechid) throws OrganizationException {\r
352                 Validator v = new Validator().nullOrBlank("mechid", mechid);\r
353                 if(v.err()) {\r
354                         return Result.err(Result.ERR_BadData,v.errs());\r
355                 }\r
356                 String ns = AAFCon.reverseDomain(mechid);\r
357                 \r
358                 String reason;\r
359                 if(trans.fish(new AAFPermission(ns + ".access", "*", "read"))\r
360                         || (reason=trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,mechid))==null) {\r
361                         return artiDAO.readByMechID(trans, mechid);\r
362                 } else {\r
363                         return Result.err(Result.ERR_Denied,reason); // note: reason is set by 2nd case, if 1st case misses\r
364                 }\r
365 \r
366         }\r
367 \r
368         public Result<List<ArtiDAO.Data>> readArtifactsByMachine(AuthzTrans trans, String machine) {\r
369                 Validator v = new Validator().nullOrBlank("machine", machine);\r
370                 if(v.err()) {\r
371                         return Result.err(Result.ERR_BadData,v.errs());\r
372                 }\r
373                 \r
374                 // TODO do some checks?\r
375 \r
376                 Result<List<ArtiDAO.Data>> rv = artiDAO.readByMachine(trans, machine);\r
377                 return rv;\r
378         }\r
379 \r
380         public Result<Void> updateArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) throws OrganizationException {\r
381                 Validator v = new Validator().artisRequired(list, 1);\r
382                 if(v.err()) {\r
383                         return Result.err(Result.ERR_BadData,v.errs());\r
384                 }\r
385                 \r
386                 // Check if requesting User is Sponsor\r
387                 //TODO - Shall we do one, or multiples?\r
388                 for(ArtiDAO.Data add : list) {\r
389                         // Policy 1: MechID must exist in Org\r
390                         Identity muser = trans.org().getIdentity(trans, add.mechid);\r
391                         if(muser == null) {\r
392                                 return Result.err(Result.ERR_Denied,"%s is not valid for %s", add.mechid,trans.org().getName());\r
393                         }\r
394                         \r
395                         // Policy 2: MechID must have valid Organization Owner\r
396                         Identity ouser = muser.owner();\r
397                         if(ouser == null) {\r
398                                 return Result.err(Result.ERR_Denied,"%s is not a valid Sponsor for %s at %s",\r
399                                                 trans.user(),add.mechid,trans.org().getName());\r
400                         }\r
401 \r
402                         // Policy 3: Renewal Days are between 10 and 60 (constants, may be parameterized)\r
403                         if(add.renewDays<MIN_RENEWAL) {\r
404                                 add.renewDays = STD_RENEWAL;\r
405                         } else if(add.renewDays>MAX_RENEWAL) {\r
406                                 add.renewDays = MAX_RENEWAL;\r
407                         }\r
408 \r
409                         // Policy 4: Data is always updated with the latest Sponsor\r
410                         // Add to Sponsor, to make sure we are always up to date.\r
411                         add.sponsor = ouser.fullID();\r
412 \r
413                         // Policy 5: If Notify is blank, set to Owner's Email\r
414                         if(add.notify==null || add.notify.length()==0) {\r
415                                 add.notify = "mailto:"+ouser.email();\r
416                         }\r
417 \r
418                         // Policy 4: only Owner may update info\r
419                         if(trans.user().equals(add.sponsor)) {\r
420                                 return artiDAO.update(trans, add);\r
421                         } else {\r
422                                 return Result.err(Result.ERR_Denied,"%s may not update info for %s",trans.user(),muser.fullID());\r
423                         }\r
424                         \r
425                 }\r
426                 return Result.err(Result.ERR_BadData,"No Artifacts to update");\r
427         }\r
428         \r
429         public Result<Void> deleteArtifact(AuthzTrans trans, String mechid, String machine) throws OrganizationException {\r
430                 Validator v = new Validator()\r
431                                 .nullOrBlank("mechid", mechid)\r
432                                 .nullOrBlank("machine", machine);\r
433                 if(v.err()) {\r
434                         return Result.err(Result.ERR_BadData,v.errs());\r
435                 }\r
436 \r
437                 Result<List<ArtiDAO.Data>> rlad = artiDAO.read(trans, mechid, machine);\r
438                 if(rlad.notOKorIsEmpty()) {\r
439                         return Result.err(Result.ERR_NotFound,"Artifact for %s %s does not exist.",mechid,machine);\r
440                 }\r
441                 \r
442                 return deleteArtifact(trans,rlad.value.get(0));\r
443         }\r
444                 \r
445         private Result<Void> deleteArtifact(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {\r
446                 // Policy 1: Record should be delete able only by Existing Sponsor.  \r
447                 String sponsor=null;\r
448                 Identity muser = trans.org().getIdentity(trans, add.mechid);\r
449                 if(muser != null) {\r
450                         Identity ouser = muser.owner();\r
451                         if(ouser!=null) {\r
452                                 sponsor = ouser.fullID();\r
453                         }\r
454                 }\r
455                 // Policy 1.a: If Sponsorship is deleted in system of Record, then \r
456                 // accept deletion by sponsor in Artifact Table\r
457                 if(sponsor==null) {\r
458                         sponsor = add.sponsor;\r
459                 }\r
460                 \r
461                 String ns = AAFCon.reverseDomain(add.mechid);\r
462 \r
463                 if(trans.fish(new AAFPermission(ns + ".access", "*", "write"))\r
464                                 || trans.user().equals(sponsor)) {\r
465                         return artiDAO.delete(trans, add, false);\r
466                 }\r
467                 return null;\r
468         }\r
469 \r
470         public Result<Void> deleteArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {\r
471                 Validator v = new Validator().artisRequired(list, 1);\r
472                 if(v.err()) {\r
473                         return Result.err(Result.ERR_BadData,v.errs());\r
474                 }\r
475 \r
476                 try {\r
477                         boolean partial = false;\r
478                         Result<Void> result=null;\r
479                         for(ArtiDAO.Data add : list) {\r
480                                 result = deleteArtifact(trans, add);\r
481                                 if(result.notOK()) {\r
482                                         partial = true;\r
483                                 }\r
484                         }\r
485                         if(result == null) {\r
486                                 result = Result.err(Result.ERR_BadData,"No Artifacts to delete"); \r
487                         } else if(partial) {\r
488                                 result.partialContent(true);\r
489                         }\r
490                         return result;\r
491                 } catch(Exception e) {\r
492                         return Result.err(e);\r
493                 }\r
494         }\r
495 \r
496         private String[] compileNotes(List<String> notes) {\r
497                 String[] rv;\r
498                 if(notes==null) {\r
499                         rv = NO_NOTES;\r
500                 } else {\r
501                         rv = new String[notes.size()];\r
502                         notes.toArray(rv);\r
503                 }\r
504                 return rv;\r
505         }\r
506 \r
507         private ByteBuffer getChallenge256SaltedHash(String challenge, int salt) throws NoSuchAlgorithmException {\r
508                 ByteBuffer bb = ByteBuffer.allocate(Integer.SIZE + challenge.length());\r
509                 bb.putInt(salt);\r
510                 bb.put(challenge.getBytes());\r
511                 byte[] hash = Hash.hashSHA256(bb.array());\r
512                 return ByteBuffer.wrap(hash);\r
513         }\r
514 }\r