2 * ============LICENSE_START====================================================
4 * ===========================================================================
5 * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
6 * Modifications Copyright (C) 2018 IBM.
7 * ===========================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END====================================================
23 package org.onap.aaf.auth.cm.service;
25 import java.io.IOException;
26 import java.net.InetAddress;
27 import java.net.UnknownHostException;
28 import java.nio.ByteBuffer;
29 import java.security.NoSuchAlgorithmException;
30 import java.security.cert.X509Certificate;
31 import java.util.ArrayList;
32 import java.util.Date;
33 import java.util.HashSet;
34 import java.util.List;
37 import org.onap.aaf.auth.cm.AAF_CM;
38 import org.onap.aaf.auth.cm.ca.CA;
39 import org.onap.aaf.auth.cm.ca.X509andChain;
40 import org.onap.aaf.auth.cm.cert.BCFactory;
41 import org.onap.aaf.auth.cm.cert.CSRMeta;
42 import org.onap.aaf.auth.cm.data.CertDrop;
43 import org.onap.aaf.auth.cm.data.CertRenew;
44 import org.onap.aaf.auth.cm.data.CertReq;
45 import org.onap.aaf.auth.cm.data.CertResp;
46 import org.onap.aaf.auth.cm.validation.CertmanValidator;
47 import org.onap.aaf.auth.dao.CassAccess;
48 import org.onap.aaf.auth.dao.cass.ArtiDAO;
49 import org.onap.aaf.auth.dao.cass.CacheInfoDAO;
50 import org.onap.aaf.auth.dao.cass.CertDAO;
51 import org.onap.aaf.auth.dao.cass.CertDAO.Data;
52 import org.onap.aaf.auth.dao.cass.CredDAO;
53 import org.onap.aaf.auth.dao.cass.HistoryDAO;
54 import org.onap.aaf.auth.dao.cass.Status;
55 import org.onap.aaf.auth.dao.hl.Question;
56 import org.onap.aaf.auth.env.AuthzTrans;
57 import org.onap.aaf.auth.layer.Result;
58 import org.onap.aaf.auth.org.Organization;
59 import org.onap.aaf.auth.org.Organization.Identity;
60 import org.onap.aaf.auth.org.OrganizationException;
61 import org.onap.aaf.cadi.Hash;
62 import org.onap.aaf.cadi.Permission;
63 import org.onap.aaf.cadi.aaf.AAFPermission;
64 import org.onap.aaf.cadi.config.Config;
65 import org.onap.aaf.cadi.configure.Factory;
66 import org.onap.aaf.cadi.util.FQI;
67 import org.onap.aaf.misc.env.APIException;
68 import org.onap.aaf.misc.env.util.Chrono;
70 public class CMService {
71 // If we add more CAs, may want to parameterize
72 private static final int STD_RENEWAL = 30;
73 private static final int MAX_RENEWAL = 60;
74 private static final int MIN_RENEWAL = 10;
76 public static final String REQUEST = "request";
77 public static final String IGNORE_IPS = "ignoreIPs";
78 public static final String RENEW = "renew";
79 public static final String DROP = "drop";
80 public static final String DOMAIN = "domain";
82 private static final String CERTMAN = "certman";
83 private static final String ACCESS = "access";
85 private static final String[] NO_NOTES = new String[0];
86 private final Permission root_read_permission;
87 private final CertDAO certDAO;
88 private final CredDAO credDAO;
89 private final ArtiDAO artiDAO;
90 private AAF_CM certManager;
92 // @SuppressWarnings("unchecked")
93 public CMService(final AuthzTrans trans, AAF_CM certman) throws APIException, IOException {
94 // Jonathan 4/2015 SessionFilter unneeded... DataStax already deals with
95 // Multithreading well
97 HistoryDAO hd = new HistoryDAO(trans, certman.cluster, CassAccess.KEYSPACE);
98 CacheInfoDAO cid = new CacheInfoDAO(trans, hd);
99 certDAO = new CertDAO(trans, hd, cid);
100 credDAO = new CredDAO(trans, hd, cid);
101 artiDAO = new ArtiDAO(trans, hd, cid);
103 this.certManager = certman;
105 root_read_permission=new AAFPermission(
106 trans.getProperty(Config.AAF_ROOT_NS, Config.AAF_ROOT_NS_DEF),
113 public Result<CertResp> requestCert(final AuthzTrans trans, final Result<CertReq> req, final CA ca) {
116 if (req.value.fqdns.isEmpty()) {
117 return Result.err(Result.ERR_BadData, "No Machines passed in Request");
120 String key = req.value.fqdns.get(0);
122 // Policy 6: Requester must be granted Change permission in Namespace requested
123 String mechNS = FQI.reverseDomain(req.value.mechid);
124 if (mechNS == null) {
125 return Result.err(Status.ERR_Denied, "%s does not reflect a valid AAF Namespace", req.value.mechid);
128 List<String> notes = null;
129 List<String> fqdns = new ArrayList<>(req.value.fqdns);
134 Organization org = trans.org();
136 boolean ignoreIPs = trans.fish(new AAFPermission(mechNS,CERTMAN, ca.getName(), IGNORE_IPS));
138 InetAddress primary = null;
139 // Organize incoming information to get to appropriate Artifact
140 if (!fqdns.isEmpty()) {
141 // Accept domain wild cards, but turn into real machines
142 // Need *domain.com:real.machine.domain.com:san.machine.domain.com:...
143 if (fqdns.get(0).startsWith("*")) { // Domain set
144 if (!trans.fish(new AAFPermission(null,ca.getPermType(), ca.getName(), DOMAIN))) {
145 return Result.err(Result.ERR_Denied,
146 "Domain based Authorizations (" + fqdns.get(0) + ") requires Exception");
149 // check for Permission in Add Artifact?
150 String domain = fqdns.get(0).substring(1);
152 if (fqdns.isEmpty()) {
153 return Result.err(Result.ERR_Denied, "Requests using domain require machine declaration");
157 InetAddress ia = InetAddress.getByName(fqdns.get(0));
159 return Result.err(Result.ERR_Denied,
160 "Request not made from matching IP matching domain");
161 } else if (ia.getHostName().endsWith(domain)) {
167 for (String cn : req.value.fqdns) {
170 InetAddress[] ias = InetAddress.getAllByName(cn);
171 Set<String> potentialSanNames = new HashSet<>();
172 for (InetAddress ia1 : ias) {
173 InetAddress ia2 = InetAddress.getByAddress(ia1.getAddress());
174 if (primary == null && ias.length == 1 && trans.ip().equals(ia1.getHostAddress())) {
176 } else if (!cn.equals(ia1.getHostName())
177 && !ia2.getHostName().equals(ia2.getHostAddress())) {
178 potentialSanNames.add(ia1.getHostName());
181 } catch (UnknownHostException e1) {
182 trans.debug().log(e1);
183 return Result.err(Result.ERR_BadData, "There is no DNS lookup for %s", cn);
192 host = req.value.fqdns.get(0);
193 } else if (primary == null) {
194 return Result.err(Result.ERR_Denied, "Request not made from matching IP (%s)", trans.ip());
196 String thost = primary.getHostName();
197 host = thost==null?primary.getHostAddress():thost;
200 ArtiDAO.Data add = null;
201 Result<List<ArtiDAO.Data>> ra = artiDAO.read(trans, req.value.mechid, host);
202 if (ra.isOKhasData()) {
204 add = ra.value.get(0); // single key
207 ra = artiDAO.read(trans, req.value.mechid, key);
208 if (ra.isOKhasData()) { // is the Template available?
209 add = ra.value.get(0);
211 for (String s : fqdns) {
212 if (!s.equals(add.machine)) {
213 add.sans(true).add(s);
216 Result<ArtiDAO.Data> rc = artiDAO.create(trans, add); // Create new Artifact from Template
218 return Result.err(rc);
221 add = ra.value.get(0);
225 // Add Artifact listed FQDNs
226 if (add.sans != null) {
227 for (String s : add.sans) {
228 if (!fqdns.contains(s)) {
234 // Policy 2: If Config marked as Expired, do not create or renew
235 Date now = new Date();
236 if (add.expires != null && now.after(add.expires)) {
237 return Result.err(Result.ERR_Policy, "Configuration for %s %s is expired %s", add.mechid,
238 add.machine, Chrono.dateFmt.format(add.expires));
241 // Policy 3: MechID must be current
242 Identity muser = org.getIdentity(trans, add.mechid);
244 return Result.err(Result.ERR_Policy, "MechID must exist in %s", org.getName());
247 // Policy 4: Sponsor must be current
248 Identity ouser = muser.responsibleTo();
250 return Result.err(Result.ERR_Policy, "%s does not have a current sponsor at %s", add.mechid,
252 } else if (!ouser.isFound() || ouser.mayOwn() != null) {
253 return Result.err(Result.ERR_Policy, "%s reports that %s cannot be responsible for %s",
254 org.getName(), trans.user());
257 // Set Email from most current Sponsor
258 email = ouser.email();
260 // Policy 5: keep Artifact data current
261 if (!ouser.fullID().equals(add.sponsor)) {
262 add.sponsor = ouser.fullID();
263 artiDAO.update(trans, add);
266 // Policy 7: Caller must be the MechID or have specifically delegated
268 if (!(trans.user().equals(req.value.mechid)
269 || trans.fish(new AAFPermission(mechNS,CERTMAN, ca.getName(), REQUEST)))) {
270 return Result.err(Status.ERR_Denied, "%s must have access to modify x509 certs in NS %s",
271 trans.user(), mechNS);
274 // Make sure Primary is the first in fqdns
275 if (fqdns.size() > 1) {
276 for (int i = 0; i < fqdns.size(); ++i) {
278 trans.error().log("CMService var primary is null");
280 String fg = fqdns.get(i);
281 if (fg!=null && fg.equals(primary.getHostName())) {
283 String tmp = fqdns.get(0);
284 fqdns.set(0, primary.getHostName());
291 } catch (Exception e) {
292 trans.debug().log(e);
293 return Result.err(Status.ERR_Denied,
294 "AppID Sponsorship cannot be determined at this time. Try later.");
299 csrMeta = BCFactory.createCSRMeta(ca, req.value.mechid, email, fqdns);
300 csrMeta.environment(ca.getEnv());
301 X509andChain x509ac = ca.sign(trans, csrMeta);
302 if (x509ac == null) {
303 return Result.err(Result.ERR_ActionNotCompleted, "x509 Certificate not signed by CA");
305 trans.info().printf("X509 Subject: %s", x509ac.getX509().getSubjectDN());
307 X509Certificate x509 = x509ac.getX509();
308 CertDAO.Data cdd = new CertDAO.Data();
309 cdd.ca = ca.getName();
310 cdd.serial = x509.getSerialNumber();
311 cdd.id = req.value.mechid;
312 cdd.x500 = x509.getSubjectDN().getName();
313 cdd.x509 = Factory.toString(trans, x509);
314 certDAO.create(trans, cdd);
316 CredDAO.Data crdd = new CredDAO.Data();
317 crdd.other = Question.random.nextInt();
318 crdd.cred = getChallenge256SaltedHash(csrMeta.challenge(), crdd.other);
319 crdd.expires = x509.getNotAfter();
320 crdd.id = req.value.mechid;
321 crdd.ns = Question.domain2ns(crdd.id);
322 crdd.type = CredDAO.CERT_SHA256_RSA;
323 crdd.tag = cdd.serial.toString(16);
324 credDAO.create(trans, crdd);
326 CertResp cr = new CertResp(trans, ca, x509, csrMeta, x509ac.getTrustChain(), compileNotes(notes));
327 return Result.ok(cr);
328 } catch (Exception e) {
329 trans.debug().log(e);
330 return Result.err(Result.ERR_ActionNotCompleted, e.getMessage());
333 return Result.err(req);
337 public Result<CertResp> renewCert(AuthzTrans trans, Result<CertRenew> renew) {
339 return Result.err(Result.ERR_NotImplemented, "Not implemented yet");
341 return Result.err(renew);
345 public Result<Void> dropCert(AuthzTrans trans, Result<CertDrop> drop) {
347 return Result.err(Result.ERR_NotImplemented, "Not implemented yet");
349 return Result.err(drop);
353 public Result<List<Data>> readCertsByMechID(AuthzTrans trans, String mechID) {
354 // Policy 1: To Read, must have NS Read or is Sponsor
355 String ns = Question.domain2ns(mechID);
357 if (trans.user().equals(mechID) || trans.fish(new AAFPermission(ns,ACCESS, "*", "read"))
358 || (trans.org().validate(trans, Organization.Policy.OWNS_MECHID, null, mechID)) == null) {
359 return certDAO.readID(trans, mechID);
361 return Result.err(Result.ERR_Denied, "%s is not the ID, Sponsor or NS Owner/Admin for %s at %s",
362 trans.user(), mechID, trans.org().getName());
364 } catch (OrganizationException e) {
365 return Result.err(e);
369 public Result<CertResp> requestPersonalCert(AuthzTrans trans, CA ca) {
370 if (ca.inPersonalDomains(trans.getUserPrincipal())) {
371 Organization org = trans.org();
373 // Policy 1: MechID must be current
376 ouser = org.getIdentity(trans, trans.user());
377 } catch (OrganizationException e1) {
378 trans.debug().log(e1);
382 return Result.err(Result.ERR_Policy, "Requesting User must exist in %s", org.getName());
385 // Set Email from most current Sponsor
389 csrMeta = BCFactory.createPersonalCSRMeta(ca, trans.user(), ouser.email());
390 X509andChain x509ac = ca.sign(trans, csrMeta);
391 if (x509ac == null) {
392 return Result.err(Result.ERR_ActionNotCompleted, "x509 Certificate not signed by CA");
394 X509Certificate x509 = x509ac.getX509();
395 CertDAO.Data cdd = new CertDAO.Data();
396 cdd.ca = ca.getName();
397 cdd.serial = x509.getSerialNumber();
398 cdd.id = trans.user();
399 cdd.x500 = x509.getSubjectDN().getName();
400 cdd.x509 = Factory.toString(trans, x509);
401 certDAO.create(trans, cdd);
403 CertResp cr = new CertResp(trans, ca, x509, csrMeta, x509ac.getTrustChain(), compileNotes(null));
404 return Result.ok(cr);
405 } catch (Exception e) {
406 trans.debug().log(e);
407 return Result.err(Result.ERR_ActionNotCompleted, e.getMessage());
410 return Result.err(Result.ERR_Denied, trans.user(), " not supported for CA", ca.getName());
417 public Result<Void> createArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {
418 CertmanValidator v = new CertmanValidator().artisRequired(list, 1);
420 return Result.err(Result.ERR_BadData, v.errs());
422 for (ArtiDAO.Data add : list) {
424 // Policy 1: MechID must exist in Org
425 Identity muser = trans.org().getIdentity(trans, add.mechid);
427 return Result.err(Result.ERR_Denied, "%s is not valid for %s", add.mechid, trans.org().getName());
430 // Policy 2: MechID must have valid Organization Owner
432 if (muser.isPerson()) {
435 Identity ouser = muser.responsibleTo();
437 return Result.err(Result.ERR_Denied, "%s is not a valid Sponsor for %s at %s", trans.user(),
438 add.mechid, trans.org().getName());
441 // Policy 3: Calling ID must be MechID Owner
442 if (!trans.user().startsWith(ouser.id())) {
443 return Result.err(Result.ERR_Denied, "%s is not the Sponsor for %s at %s", trans.user(),
444 add.mechid, trans.org().getName());
449 // Policy 4: Renewal Days are between 10 and 60 (constants, may be
451 if (add.renewDays < MIN_RENEWAL) {
452 add.renewDays = STD_RENEWAL;
453 } else if (add.renewDays > MAX_RENEWAL) {
454 add.renewDays = MAX_RENEWAL;
457 // Policy 5: If Notify is blank, set to Owner's Email
458 if (add.notify == null || add.notify.length() == 0) {
459 add.notify = "mailto:" + emailUser.email();
462 // Policy 6: Only do Domain by Exception
463 if (add.machine.startsWith("*")) { // Domain set
464 CA ca = certManager.getCA(add.ca);
466 if (!trans.fish(new AAFPermission(ca.getPermNS(),ca.getPermType(), add.ca, DOMAIN))) {
467 return Result.err(Result.ERR_Denied, "Domain Artifacts (%s) requires specific Permission",
472 // Set Sponsor from Golden Source
473 add.sponsor = emailUser.fullID();
475 } catch (OrganizationException e) {
476 return Result.err(e);
479 Result<ArtiDAO.Data> rv = artiDAO.create(trans, add);
480 // come up with Partial Reporting Scheme, or allow only one at a time.
482 return Result.err(rv);
488 public Result<List<ArtiDAO.Data>> readArtifacts(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {
489 CertmanValidator v = new CertmanValidator().keys(add);
491 return Result.err(Result.ERR_BadData, v.errs());
493 Result<List<ArtiDAO.Data>> data = artiDAO.read(trans, add);
494 if (data.notOKorIsEmpty()) {
497 add = data.value.get(0);
498 if (trans.user().equals(add.mechid)
499 || trans.fish(root_read_permission,
500 new AAFPermission(add.ns,ACCESS, "*", "read"),
501 new AAFPermission(add.ns,CERTMAN, add.ca, "read"),
502 new AAFPermission(add.ns,CERTMAN, add.ca, REQUEST))
503 || (trans.org().validate(trans, Organization.Policy.OWNS_MECHID, null, add.mechid)) == null) {
506 return Result.err(Result.ERR_Denied,
507 "%s is not %s, is not the sponsor, and doesn't have delegated permission.", trans.user(),
508 add.mechid, add.ns + ".certman|" + add.ca + "|read or ...|request"); // note: reason is set by 2nd
509 // case, if 1st case misses
514 public Result<List<ArtiDAO.Data>> readArtifactsByMechID(AuthzTrans trans, String mechid)
515 throws OrganizationException {
516 CertmanValidator v = new CertmanValidator();
517 v.nullOrBlank("mechid", mechid);
519 return Result.err(Result.ERR_BadData, v.errs());
521 String ns = FQI.reverseDomain(mechid);
524 if (trans.fish(new AAFPermission(ns, ACCESS, "*", "read"))
525 || (reason = trans.org().validate(trans, Organization.Policy.OWNS_MECHID, null, mechid)) == null) {
526 return artiDAO.readByMechID(trans, mechid);
528 return Result.err(Result.ERR_Denied, reason); // note: reason is set by 2nd case, if 1st case misses
533 public Result<List<ArtiDAO.Data>> readArtifactsByMachine(AuthzTrans trans, String machine) {
534 CertmanValidator v = new CertmanValidator();
535 v.nullOrBlank("machine", machine);
537 return Result.err(Result.ERR_BadData, v.errs());
542 return artiDAO.readByMachine(trans, machine);
545 public Result<List<ArtiDAO.Data>> readArtifactsByNs(AuthzTrans trans, String ns) {
546 CertmanValidator v = new CertmanValidator();
547 v.nullOrBlank("ns", ns);
549 return Result.err(Result.ERR_BadData, v.errs());
553 return artiDAO.readByNs(trans, ns);
556 public Result<Void> updateArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) throws OrganizationException {
557 CertmanValidator v = new CertmanValidator();
558 v.artisRequired(list, 1);
560 return Result.err(Result.ERR_BadData, v.errs());
563 // Check if requesting User is Sponsor
564 // Shall we do one, or multiples?
565 for (ArtiDAO.Data add : list) {
566 // Policy 1: MechID must exist in Org
567 Identity muser = trans.org().getIdentity(trans, add.mechid);
569 return Result.err(Result.ERR_Denied, "%s is not valid for %s", add.mechid, trans.org().getName());
572 // Policy 2: MechID must have valid Organization Owner
573 Identity ouser = muser.responsibleTo();
575 return Result.err(Result.ERR_Denied, "%s is not a valid Sponsor for %s at %s", trans.user(), add.mechid,
576 trans.org().getName());
579 // Policy 3: Renewal Days are between 10 and 60 (constants, may be
581 if (add.renewDays < MIN_RENEWAL) {
582 add.renewDays = STD_RENEWAL;
583 } else if (add.renewDays > MAX_RENEWAL) {
584 add.renewDays = MAX_RENEWAL;
587 // Policy 4: Data is always updated with the latest Sponsor
588 // Add to Sponsor, to make sure we are always up to date.
589 add.sponsor = ouser.fullID();
591 // Policy 5: If Notify is blank, set to Owner's Email
592 if (add.notify == null || add.notify.length() == 0) {
593 add.notify = "mailto:" + ouser.email();
595 // Policy 6: Only do Domain by Exception
596 if (add.machine.startsWith("*")) { // Domain set
597 CA ca = certManager.getCA(add.ca);
599 return Result.err(Result.ERR_BadData, "CA is required in Artifact");
601 if (!trans.fish(new AAFPermission(null,ca.getPermType(), add.ca, DOMAIN))) {
602 return Result.err(Result.ERR_Denied, "Domain Artifacts (%s) requires specific Permission",
607 // Policy 7: only Owner may update info
608 if (trans.user().startsWith(ouser.id())) {
609 return artiDAO.update(trans, add);
611 return Result.err(Result.ERR_Denied, "%s may not update info for %s", trans.user(), muser.fullID());
614 return Result.err(Result.ERR_BadData, "No Artifacts to update");
617 public Result<Void> deleteArtifact(AuthzTrans trans, String mechid, String machine) throws OrganizationException {
618 CertmanValidator v = new CertmanValidator();
619 v.nullOrBlank("mechid", mechid).nullOrBlank("machine", machine);
621 return Result.err(Result.ERR_BadData, v.errs());
624 Result<List<ArtiDAO.Data>> rlad = artiDAO.read(trans, mechid, machine);
625 if (rlad.notOKorIsEmpty()) {
626 return Result.err(Result.ERR_NotFound, "Artifact for %s %s does not exist.", mechid, machine);
629 return deleteArtifact(trans, rlad.value.get(0));
632 private Result<Void> deleteArtifact(AuthzTrans trans, ArtiDAO.Data add) throws OrganizationException {
633 // Policy 1: Record should be delete able only by Existing Sponsor.
634 String sponsor = null;
635 Identity muser = trans.org().getIdentity(trans, add.mechid);
637 Identity ouser = muser.responsibleTo();
639 sponsor = ouser.fullID();
642 // Policy 1.a: If Sponsorship is deleted in system of Record, then
643 // accept deletion by sponsor in Artifact Table
644 if (sponsor == null) {
645 sponsor = add.sponsor;
648 String ns = FQI.reverseDomain(add.mechid);
650 if (trans.fish(new AAFPermission(ns,ACCESS, "*", "write")) || trans.user().equals(sponsor)) {
651 return artiDAO.delete(trans, add, false);
653 return Result.err(Result.ERR_Denied, "%1 is not allowed to delete this item", trans.user());
656 public Result<Void> deleteArtifact(AuthzTrans trans, List<ArtiDAO.Data> list) {
657 CertmanValidator v = new CertmanValidator().artisRequired(list, 1);
659 return Result.err(Result.ERR_BadData, v.errs());
663 boolean partial = false;
664 Result<Void> result = null;
665 for (ArtiDAO.Data add : list) {
666 result = deleteArtifact(trans, add);
667 if (result.notOK()) {
671 if (result == null) {
672 result = Result.err(Result.ERR_BadData, "No Artifacts to delete");
673 } else if (partial) {
674 result.partialContent(true);
677 } catch (Exception e) {
678 return Result.err(e);
682 private String[] compileNotes(List<String> notes) {
687 rv = new String[notes.size()];
693 private ByteBuffer getChallenge256SaltedHash(String challenge, int salt) throws NoSuchAlgorithmException {
694 ByteBuffer bb = ByteBuffer.allocate(Integer.SIZE + challenge.length());
696 bb.put(challenge.getBytes());
697 byte[] hash = Hash.hashSHA256(bb.array());
698 return ByteBuffer.wrap(hash);