2 * ============LICENSE_START====================================================
4 * ===========================================================================
5 * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
6 * ===========================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END====================================================
22 package org.onap.aaf.auth.cm.service;
24 import java.io.IOException;
25 import java.net.InetAddress;
26 import java.net.UnknownHostException;
27 import java.nio.ByteBuffer;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.cert.X509Certificate;
30 import java.util.ArrayList;
31 import java.util.Date;
32 import java.util.HashSet;
33 import java.util.List;
36 import org.onap.aaf.auth.cm.AAF_CM;
37 import org.onap.aaf.auth.cm.ca.CA;
38 import org.onap.aaf.auth.cm.ca.X509andChain;
39 import org.onap.aaf.auth.cm.cert.BCFactory;
40 import org.onap.aaf.auth.cm.cert.CSRMeta;
41 import org.onap.aaf.auth.cm.data.CertDrop;
42 import org.onap.aaf.auth.cm.data.CertRenew;
43 import org.onap.aaf.auth.cm.data.CertReq;
44 import org.onap.aaf.auth.cm.data.CertResp;
45 import org.onap.aaf.auth.cm.validation.CertmanValidator;
46 import org.onap.aaf.auth.dao.CassAccess;
47 import org.onap.aaf.auth.dao.cass.ArtiDAO;
48 import org.onap.aaf.auth.dao.cass.CacheInfoDAO;
49 import org.onap.aaf.auth.dao.cass.CertDAO;
50 import org.onap.aaf.auth.dao.cass.CertDAO.Data;
51 import org.onap.aaf.auth.dao.cass.CredDAO;
52 import org.onap.aaf.auth.dao.cass.HistoryDAO;
53 import org.onap.aaf.auth.dao.cass.Status;
54 import org.onap.aaf.auth.dao.hl.Question;
55 import org.onap.aaf.auth.env.AuthzTrans;
56 import org.onap.aaf.auth.layer.Result;
57 import org.onap.aaf.auth.org.Organization;
58 import org.onap.aaf.auth.org.Organization.Identity;
59 import org.onap.aaf.auth.org.OrganizationException;
60 import org.onap.aaf.cadi.Hash;
61 import org.onap.aaf.cadi.aaf.AAFPermission;
62 import org.onap.aaf.cadi.configure.Factory;
63 import org.onap.aaf.cadi.util.FQI;
64 import org.onap.aaf.misc.env.APIException;
65 import org.onap.aaf.misc.env.util.Chrono;
68 public class CMService {
69 // If we add more CAs, may want to parameterize
70 private static final int STD_RENEWAL = 30;
71 private static final int MAX_RENEWAL = 60;
72 private static final int MIN_RENEWAL = 10;
74 public static final String REQUEST = "request";
75 public static final String RENEW = "renew";
76 public static final String DROP = "drop";
77 public static final String IPS = "ips";
78 public static final String DOMAIN = "domain";
80 private static final String CERTMAN = ".certman";
81 private static final String ACCESS = ".access";
83 private static final String[] NO_NOTES = new String[0];
84 private final CertDAO certDAO;
85 private final CredDAO credDAO;
86 private final ArtiDAO artiDAO;
87 private AAF_CM certman;
89 // @SuppressWarnings("unchecked")
90 public CMService(final AuthzTrans trans, AAF_CM certman) throws APIException, IOException {
91 // Jonathan 4/2015 SessionFilter unneeded... DataStax already deals with Multithreading well
93 HistoryDAO hd = new HistoryDAO(trans, certman.cluster, CassAccess.KEYSPACE);
94 CacheInfoDAO cid = new CacheInfoDAO(trans, hd);
95 certDAO = new CertDAO(trans, hd, cid);
96 credDAO = new CredDAO(trans, hd, cid);
97 artiDAO = new ArtiDAO(trans, hd, cid);
99 this.certman = certman;
102 public Result<CertResp> requestCert(final AuthzTrans trans,final Result<CertReq> req, final CA ca) {
105 if(req.value.fqdns.isEmpty()) {
106 return Result.err(Result.ERR_BadData,"No Machines passed in Request");
109 String key = req.value.fqdns.get(0);
111 // Policy 6: Requester must be granted Change permission in Namespace requested
112 String mechNS = FQI.reverseDomain(req.value.mechid);
114 return Result.err(Status.ERR_Denied, "%s does not reflect a valid AAF Namespace",req.value.mechid);
118 // Disallow non-AAF CA without special permission
119 if(!"aaf".equals(ca.getName()) && !trans.fish( new AAFPermission(mechNS+CERTMAN, ca.getName(), REQUEST))) {
120 return Result.err(Status.ERR_Denied, "'%s' does not have permission to request Certificates from Certificate Authority '%s'",
121 trans.user(),ca.getName());
124 List<String> notes = null;
125 List<String> fqdns = new ArrayList<>(req.value.fqdns);
131 Organization org = trans.org();
133 InetAddress primary = null;
134 // Organize incoming information to get to appropriate Artifact
135 if(!fqdns.isEmpty()) {
136 // Accept domain wild cards, but turn into real machines
137 // Need *domain.com:real.machine.domain.com:san.machine.domain.com:...
138 if(fqdns.get(0).startsWith("*")) { // Domain set
139 if(!trans.fish(new AAFPermission(ca.getPermType(), ca.getName(), DOMAIN))) {
140 return Result.err(Result.ERR_Denied, "Domain based Authorizations (" + fqdns.get(0) + ") requires Exception");
143 //TODO check for Permission in Add Artifact?
144 String domain = fqdns.get(0).substring(1);
146 if(fqdns.isEmpty()) {
147 return Result.err(Result.ERR_Denied, "Requests using domain require machine declaration");
150 InetAddress ia = InetAddress.getByName(fqdns.get(0));
152 return Result.err(Result.ERR_Denied, "Request not made from matching IP matching domain");
153 } else if(ia.getHostName().endsWith(domain)) {
158 for(String cn : req.value.fqdns) {
160 InetAddress[] ias = InetAddress.getAllByName(cn);
161 Set<String> potentialSanNames = new HashSet<>();
162 for(InetAddress ia1 : ias) {
163 InetAddress ia2 = InetAddress.getByAddress(ia1.getAddress());
164 if(primary==null && ias.length==1 && trans.ip().equals(ia1.getHostAddress())) {
166 } else if(!cn.equals(ia1.getHostName()) && !ia2.getHostName().equals(ia2.getHostAddress())) {
167 potentialSanNames.add(ia1.getHostName());
170 } catch (UnknownHostException e1) {
171 return Result.err(Result.ERR_BadData,"There is no DNS lookup for %s",cn);
179 return Result.err(Result.ERR_Denied, "Request not made from matching IP (%s)",trans.ip());
182 ArtiDAO.Data add = null;
183 Result<List<ArtiDAO.Data>> ra = artiDAO.read(trans, req.value.mechid,primary.getHostAddress());
184 if(ra.isOKhasData()) {
186 add = ra.value.get(0); // single key
189 ra = artiDAO.read(trans, req.value.mechid,key);
190 if(ra.isOKhasData()) { // is the Template available?
191 add = ra.value.get(0);
192 add.machine=primary.getHostName();
193 for(String s : fqdns) {
194 if(!s.equals(add.machine)) {
195 add.sans(true).add(s);
198 Result<ArtiDAO.Data> rc = artiDAO.create(trans, add); // Create new Artifact from Template
200 return Result.err(rc);
203 add = ra.value.get(0);
207 // Add Artifact listed FQDNs
209 for(String s : add.sans) {
210 if(!fqdns.contains(s)) {
216 // Policy 2: If Config marked as Expired, do not create or renew
217 Date now = new Date();
218 if(add.expires!=null && now.after(add.expires)) {
219 return Result.err(Result.ERR_Policy,"Configuration for %s %s is expired %s",add.mechid,add.machine,Chrono.dateFmt.format(add.expires));
222 // Policy 3: MechID must be current
223 Identity muser = org.getIdentity(trans, add.mechid);
225 return Result.err(Result.ERR_Policy,"MechID must exist in %s",org.getName());
228 // Policy 4: Sponsor must be current
229 Identity ouser = muser.responsibleTo();
231 return Result.err(Result.ERR_Policy,"%s does not have a current sponsor at %s",add.mechid,org.getName());
232 } else if(!ouser.isFound() || ouser.mayOwn()!=null) {
233 return Result.err(Result.ERR_Policy,"%s reports that %s cannot be responsible for %s",org.getName(),trans.user());
236 // Set Email from most current Sponsor
237 email = ouser.email();
239 // Policy 5: keep Artifact data current
240 if(!ouser.fullID().equals(add.sponsor)) {
241 add.sponsor = ouser.fullID();
242 artiDAO.update(trans, add);
245 // Policy 7: Caller must be the MechID or have specifically delegated permissions
246 if(!(trans.user().equals(req.value.mechid) || trans.fish(new AAFPermission(mechNS + CERTMAN, ca.getName() , REQUEST)))) {
247 return Result.err(Status.ERR_Denied, "%s must have access to modify x509 certs in NS %s",trans.user(),mechNS);
250 // Make sure Primary is the first in fqdns
252 for(int i=0;i<fqdns.size();++i) {
253 if(fqdns.get(i).equals(primary.getHostName())) {
255 String tmp = fqdns.get(0);
256 fqdns.set(0, primary.getHostName());
262 } catch (Exception e) {
263 trans.error().log(e);
264 return Result.err(Status.ERR_Denied,"MechID Sponsorship cannot be determined at this time. Try later");
269 csrMeta = BCFactory.createCSRMeta(
274 X509andChain x509ac = ca.sign(trans, csrMeta);
276 return Result.err(Result.ERR_ActionNotCompleted,"x509 Certificate not signed by CA");
278 trans.info().printf("X509 Subject: %s", x509ac.getX509().getSubjectDN());
280 X509Certificate x509 = x509ac.getX509();
281 CertDAO.Data cdd = new CertDAO.Data();
283 cdd.serial=x509.getSerialNumber();
284 cdd.id=req.value.mechid;
285 cdd.x500=x509.getSubjectDN().getName();
286 cdd.x509=Factory.toString(trans, x509);
287 certDAO.create(trans, cdd);
289 CredDAO.Data crdd = new CredDAO.Data();
290 crdd.other = Question.random.nextInt();
291 crdd.cred=getChallenge256SaltedHash(csrMeta.challenge(),crdd.other);
292 crdd.expires = x509.getNotAfter();
293 crdd.id = req.value.mechid;
294 crdd.ns = Question.domain2ns(crdd.id);
295 crdd.type = CredDAO.CERT_SHA256_RSA;
296 credDAO.create(trans, crdd);
298 CertResp cr = new CertResp(trans, ca, x509, csrMeta, x509ac.getTrustChain(),compileNotes(notes));
299 return Result.ok(cr);
300 } catch (Exception e) {
301 trans.error().log(e);
302 return Result.err(Result.ERR_ActionNotCompleted,e.getMessage());
305 return Result.err(req);
309 public Result<CertResp> renewCert(AuthzTrans trans, Result<CertRenew> renew) {
311 return Result.err(Result.ERR_NotImplemented,"Not implemented yet");
313 return Result.err(renew);
317 public Result<Void> dropCert(AuthzTrans trans, Result<CertDrop> drop) {
319 return Result.err(Result.ERR_NotImplemented,"Not implemented yet");
321 return Result.err(drop);
325 public Result<List<Data>> readCertsByMechID(AuthzTrans trans, String mechID) {
326 // Policy 1: To Read, must have NS Read or is Sponsor
327 String ns = Question.domain2ns(mechID);
329 if( trans.user().equals(mechID)
330 || trans.fish(new AAFPermission(ns + ACCESS, "*", "read"))
331 || (trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,mechID))==null) {
332 return certDAO.readID(trans, mechID);
334 return Result.err(Result.ERR_Denied,"%s is not the ID, Sponsor or NS Owner/Admin for %s at %s",
335 trans.user(),mechID,trans.org().getName());
337 } catch(OrganizationException e) {
338 return Result.err(e);
342 public Result<CertResp> requestPersonalCert(AuthzTrans trans, CA ca) {
343 if(ca.inPersonalDomains(trans.getUserPrincipal())) {
344 Organization org = trans.org();
346 // Policy 1: MechID must be current
349 ouser = org.getIdentity(trans, trans.user());
350 } catch (OrganizationException e1) {
351 trans.error().log(e1);
355 return Result.err(Result.ERR_Policy,"Requesting User must exist in %s",org.getName());
358 // Set Email from most current Sponsor
362 csrMeta = BCFactory.createPersonalCSRMeta(
366 X509andChain x509ac = ca.sign(trans, csrMeta);
368 return Result.err(Result.ERR_ActionNotCompleted,"x509 Certificate not signed by CA");
370 X509Certificate x509 = x509ac.getX509();
371 CertDAO.Data cdd = new CertDAO.Data();
373 cdd.serial=x509.getSerialNumber();
375 cdd.x500=x509.getSubjectDN().getName();
376 cdd.x509=Factory.toString(trans, x509);
377 certDAO.create(trans, cdd);
379 CertResp cr = new CertResp(trans, ca, x509, csrMeta, x509ac.getTrustChain(), compileNotes(null));
380 return Result.ok(cr);
381 } catch (Exception e) {
382 trans.error().log(e);
383 return Result.err(Result.ERR_ActionNotCompleted,e.getMessage());
386 return Result.err(Result.ERR_Denied,trans.user()," not supported for CA",ca.getName());
393 public Result<Void> createArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {
394 CertmanValidator v = new CertmanValidator().artisRequired(list, 1);
396 return Result.err(Result.ERR_BadData,v.errs());
398 for(ArtiDAO.Data add : list) {
400 // Policy 1: MechID must exist in Org
401 Identity muser = trans.org().getIdentity(trans, add.mechid);
403 return Result.err(Result.ERR_Denied,"%s is not valid for %s", add.mechid,trans.org().getName());
406 // Policy 2: MechID must have valid Organization Owner
408 if(muser.isPerson()) {
411 Identity ouser = muser.responsibleTo();
413 return Result.err(Result.ERR_Denied,"%s is not a valid Sponsor for %s at %s",
414 trans.user(),add.mechid,trans.org().getName());
417 // Policy 3: Calling ID must be MechID Owner
418 if(!trans.user().equals(ouser.fullID())) {
419 return Result.err(Result.ERR_Denied,"%s is not the Sponsor for %s at %s",
420 trans.user(),add.mechid,trans.org().getName());
426 // Policy 4: Renewal Days are between 10 and 60 (constants, may be parameterized)
427 if(add.renewDays<MIN_RENEWAL) {
428 add.renewDays = STD_RENEWAL;
429 } else if(add.renewDays>MAX_RENEWAL) {
430 add.renewDays = MAX_RENEWAL;
433 // Policy 5: If Notify is blank, set to Owner's Email
434 if(add.notify==null || add.notify.length()==0) {
435 add.notify = "mailto:"+emailUser.email();
438 // Policy 6: Only do Domain by Exception
439 if(add.machine.startsWith("*")) { // Domain set
440 CA ca = certman.getCA(add.ca);
443 if(!trans.fish(new AAFPermission(ca.getPermType(), add.ca, DOMAIN))) {
444 return Result.err(Result.ERR_Denied,"Domain Artifacts (%s) requires specific Permission",
449 // Set Sponsor from Golden Source
450 add.sponsor = emailUser.fullID();
453 } catch (OrganizationException e) {
454 return Result.err(e);
457 Result<ArtiDAO.Data> rv = artiDAO.create(trans, add);
458 // TODO come up with Partial Reporting Scheme, or allow only one at a time.
460 return Result.err(rv);
466 public Result<List<ArtiDAO.Data>> readArtifacts(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {
467 CertmanValidator v = new CertmanValidator().keys(add);
469 return Result.err(Result.ERR_BadData,v.errs());
471 Result<List<ArtiDAO.Data>> data = artiDAO.read(trans, add);
472 if(data.notOKorIsEmpty()) {
475 add = data.value.get(0);
476 if( trans.user().equals(add.mechid)
477 || trans.fish(new AAFPermission(add.ns + ACCESS, "*", "read"))
478 || trans.fish(new AAFPermission(add.ns+CERTMAN,add.ca,"read"))
479 || trans.fish(new AAFPermission(add.ns+CERTMAN,add.ca,"request"))
480 || (trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,add.mechid))==null) {
483 return Result.err(Result.ERR_Denied,"%s is not %s, is not the sponsor, and doesn't have delegated permission.",trans.user(),add.mechid,add.ns+".certman|"+add.ca+"|read or ...|request"); // note: reason is set by 2nd case, if 1st case misses
488 public Result<List<ArtiDAO.Data>> readArtifactsByMechID(AuthzTrans trans, String mechid) throws OrganizationException {
489 CertmanValidator v = new CertmanValidator();
490 v.nullOrBlank("mechid", mechid);
492 return Result.err(Result.ERR_BadData,v.errs());
494 String ns = FQI.reverseDomain(mechid);
497 if(trans.fish(new AAFPermission(ns + ACCESS, "*", "read"))
498 || (reason=trans.org().validate(trans,Organization.Policy.OWNS_MECHID,null,mechid))==null) {
499 return artiDAO.readByMechID(trans, mechid);
501 return Result.err(Result.ERR_Denied,reason); // note: reason is set by 2nd case, if 1st case misses
506 public Result<List<ArtiDAO.Data>> readArtifactsByMachine(AuthzTrans trans, String machine) {
507 CertmanValidator v = new CertmanValidator();
508 v.nullOrBlank("machine", machine);
510 return Result.err(Result.ERR_BadData,v.errs());
513 // TODO do some checks?
515 Result<List<ArtiDAO.Data>> rv = artiDAO.readByMachine(trans, machine);
519 public Result<List<ArtiDAO.Data>> readArtifactsByNs(AuthzTrans trans, String ns) {
520 CertmanValidator v = new CertmanValidator();
521 v.nullOrBlank("ns", ns);
523 return Result.err(Result.ERR_BadData,v.errs());
526 // TODO do some checks?
528 return artiDAO.readByNs(trans, ns);
532 public Result<Void> updateArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) throws OrganizationException {
533 CertmanValidator v = new CertmanValidator();
534 v.artisRequired(list, 1);
536 return Result.err(Result.ERR_BadData,v.errs());
539 // Check if requesting User is Sponsor
540 //TODO - Shall we do one, or multiples?
541 for(ArtiDAO.Data add : list) {
542 // Policy 1: MechID must exist in Org
543 Identity muser = trans.org().getIdentity(trans, add.mechid);
545 return Result.err(Result.ERR_Denied,"%s is not valid for %s", add.mechid,trans.org().getName());
548 // Policy 2: MechID must have valid Organization Owner
549 Identity ouser = muser.responsibleTo();
551 return Result.err(Result.ERR_Denied,"%s is not a valid Sponsor for %s at %s",
552 trans.user(),add.mechid,trans.org().getName());
555 // Policy 3: Renewal Days are between 10 and 60 (constants, may be parameterized)
556 if(add.renewDays<MIN_RENEWAL) {
557 add.renewDays = STD_RENEWAL;
558 } else if(add.renewDays>MAX_RENEWAL) {
559 add.renewDays = MAX_RENEWAL;
562 // Policy 4: Data is always updated with the latest Sponsor
563 // Add to Sponsor, to make sure we are always up to date.
564 add.sponsor = ouser.fullID();
566 // Policy 5: If Notify is blank, set to Owner's Email
567 if(add.notify==null || add.notify.length()==0) {
568 add.notify = "mailto:"+ouser.email();
570 // Policy 6: Only do Domain by Exception
571 if(add.machine.startsWith("*")) { // Domain set
572 CA ca = certman.getCA(add.ca);
574 return Result.err(Result.ERR_BadData, "CA is required in Artifact");
576 if(!trans.fish(new AAFPermission(ca.getPermType(), add.ca, DOMAIN))) {
577 return Result.err(Result.ERR_Denied,"Domain Artifacts (%s) requires specific Permission",
582 // Policy 7: only Owner may update info
583 if(trans.user().equals(add.sponsor)) {
584 return artiDAO.update(trans, add);
586 return Result.err(Result.ERR_Denied,"%s may not update info for %s",trans.user(),muser.fullID());
589 return Result.err(Result.ERR_BadData,"No Artifacts to update");
592 public Result<Void> deleteArtifact(AuthzTrans trans, String mechid, String machine) throws OrganizationException {
593 CertmanValidator v = new CertmanValidator();
594 v.nullOrBlank("mechid", mechid)
595 .nullOrBlank("machine", machine);
597 return Result.err(Result.ERR_BadData,v.errs());
600 Result<List<ArtiDAO.Data>> rlad = artiDAO.read(trans, mechid, machine);
601 if(rlad.notOKorIsEmpty()) {
602 return Result.err(Result.ERR_NotFound,"Artifact for %s %s does not exist.",mechid,machine);
605 return deleteArtifact(trans,rlad.value.get(0));
608 private Result<Void> deleteArtifact(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {
609 // Policy 1: Record should be delete able only by Existing Sponsor.
611 Identity muser = trans.org().getIdentity(trans, add.mechid);
613 Identity ouser = muser.responsibleTo();
615 sponsor = ouser.fullID();
618 // Policy 1.a: If Sponsorship is deleted in system of Record, then
619 // accept deletion by sponsor in Artifact Table
621 sponsor = add.sponsor;
624 String ns = FQI.reverseDomain(add.mechid);
626 if(trans.fish(new AAFPermission(ns + ACCESS, "*", "write"))
627 || trans.user().equals(sponsor)) {
628 return artiDAO.delete(trans, add, false);
630 return Result.err(Result.ERR_Denied, "%1 is not allowed to delete this item",trans.user());
633 public Result<Void> deleteArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {
634 CertmanValidator v = new CertmanValidator().artisRequired(list, 1);
636 return Result.err(Result.ERR_BadData,v.errs());
640 boolean partial = false;
641 Result<Void> result=null;
642 for(ArtiDAO.Data add : list) {
643 result = deleteArtifact(trans, add);
649 result = Result.err(Result.ERR_BadData,"No Artifacts to delete");
651 result.partialContent(true);
654 } catch(Exception e) {
655 return Result.err(e);
659 private String[] compileNotes(List<String> notes) {
664 rv = new String[notes.size()];
670 private ByteBuffer getChallenge256SaltedHash(String challenge, int salt) throws NoSuchAlgorithmException {
671 ByteBuffer bb = ByteBuffer.allocate(Integer.SIZE + challenge.length());
673 bb.put(challenge.getBytes());
674 byte[] hash = Hash.hashSHA256(bb.array());
675 return ByteBuffer.wrap(hash);