Minor changes for Organization needs
[aaf/authz.git] / auth / auth-deforg / src / main / java / org / onap / aaf / org / DefaultOrg.java
1 /*******************************************************************************
2  * ============LICENSE_START====================================================
3  * * org.onap.aaf
4  * * ===========================================================================
5  * * Copyright © 2017 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
10  * *
11  *  *      http://www.apache.org/licenses/LICENSE-2.0
12  * *
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====================================================
19  * *
20  * *
21  ******************************************************************************/
22 package org.onap.aaf.org;
23
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Date;
28 import java.util.GregorianCalendar;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Set;
32 import java.util.regex.Pattern;
33
34 import org.onap.aaf.auth.env.AuthzTrans;
35 import org.onap.aaf.auth.local.AbsData.Reuse;
36 import org.onap.aaf.auth.org.EmailWarnings;
37 import org.onap.aaf.auth.org.Executor;
38 import org.onap.aaf.auth.org.Mailer;
39 import org.onap.aaf.auth.org.Organization;
40 import org.onap.aaf.auth.org.OrganizationException;
41 import org.onap.aaf.cadi.config.Config;
42 import org.onap.aaf.cadi.util.FQI;
43 import org.onap.aaf.misc.env.Env;
44
45 public class DefaultOrg implements Organization {
46     private static final String AAF_DATA_DIR = "aaf_data_dir";
47     // Package on Purpose
48     final String domain;
49     final String atDomain;
50     final String realm;
51
52     private final String root_ns;
53
54     private final String NAME;
55     private final Set<String> supportedRealms;
56
57
58
59     public DefaultOrg(Env env, String realm) throws OrganizationException {
60
61         this.realm = realm;
62         supportedRealms=new HashSet<>();
63         supportedRealms.add(realm);
64         domain=FQI.reverseDomain(realm);
65         atDomain = '@'+domain;
66         NAME=env.getProperty(realm + ".name","Default Organization");
67         root_ns = env.getProperty(Config.AAF_ROOT_NS,Config.AAF_ROOT_NS_DEF);
68
69         try {
70             String defFile;
71             String temp=env.getProperty(defFile = (getClass().getName()+".file"));
72             File fIdentities=null;
73             if (temp==null) {
74                 temp = env.getProperty(AAF_DATA_DIR);
75                 if (temp!=null) {
76                     env.warn().log(defFile, " is not defined. Using default: ",temp+"/identities.dat");
77                     File dir = new File(temp);
78                     fIdentities=new File(dir,"identities.dat");
79
80                     if (!fIdentities.exists()) {
81                         env.warn().log("No",fIdentities.getCanonicalPath(),"exists.  Creating.");
82                         if (!dir.exists()) {
83                             dir.mkdirs();
84                         }
85                         fIdentities.createNewFile();
86                     }
87
88                 }
89             } else {
90                 fIdentities = new File(temp);
91                 if (!fIdentities.exists()) {
92                     String dataDir = env.getProperty(AAF_DATA_DIR);
93                     if (dataDir!=null) {
94                         fIdentities = new File(dataDir,temp);
95                     }
96                 }
97             }
98
99             if (fIdentities!=null && fIdentities.exists()) {
100                 identities = new Identities(fIdentities);
101             } else {
102                 if (fIdentities==null) {
103                     throw new OrganizationException("No Identities: set \"" + AAF_DATA_DIR + '"');
104                 } else {
105                     throw new OrganizationException(fIdentities.getCanonicalPath() + " does not exist.");
106                 }
107             }
108
109             File fRevoked=null;
110             temp=env.getProperty(getClass().getName()+".file.revoked");
111             if(temp==null) {
112                 temp = env.getProperty(AAF_DATA_DIR);
113                 if (temp!=null) {
114                     File dir = new File(temp);
115                     fRevoked=new File(dir,"revoked.dat");
116                 }
117             } else {
118                 fRevoked = new File(temp);
119             }
120             if (fRevoked!=null && fRevoked.exists()) {
121                 revoked = new Identities(fRevoked);
122             } else {
123                 revoked = null;
124             }
125
126         } catch (IOException e) {
127             throw new OrganizationException(e);
128         }
129     }
130
131     // Implement your own Delegation System
132     static final List<String> NULL_DELEGATES = new ArrayList<>();
133
134     public Identities identities;
135     public Identities revoked;
136     private boolean dryRun;
137     private Mailer mailer;
138     public enum Types {Employee, Contractor, Application, NotActive};
139     private final static Set<String> typeSet;
140
141     static {
142         typeSet = new HashSet<>();
143         for (Types t : Types.values()) {
144             typeSet.add(t.name());
145         }
146     }
147
148     private static final EmailWarnings emailWarnings = new DefaultOrgWarnings();
149
150     @Override
151     public String getName() {
152         return NAME;
153     }
154
155     @Override
156     public String getRealm() {
157         return realm;
158     }
159
160     @Override
161     public String getDomain() {
162         return domain;
163     }
164
165     @Override
166     public DefaultOrgIdentity getIdentity(AuthzTrans trans, String id) throws OrganizationException {
167         int at = id.indexOf('@');
168         return new DefaultOrgIdentity(trans,at<0?id:id.substring(0, at),this);
169     }
170
171     /**
172      * If the ID isn't in the revoked file, if it exists, it is revoked.
173      */
174     @Override
175     public boolean isRevoked(AuthzTrans trans, String key) {
176         if(revoked!=null) {
177             try {
178                 revoked.open(trans, DefaultOrgIdentity.TIMEOUT);
179                 try {
180                     Reuse r = revoked.reuse();
181                     int at = key.indexOf(domain);
182                     String search;
183                     if (at>=0) {
184                         search = key.substring(0,at);
185                     } else {
186                         search = key;
187                     }
188                     return revoked.find(search, r)!=null;
189                 } finally {
190                     revoked.close(trans);
191                 }
192             } catch (IOException e) {
193                 trans.error().log(e);
194             }
195         }
196         return false;
197     }
198
199     /* (non-Javadoc)
200      * @see org.onap.aaf.auth.org.Organization#getEsclaations(org.onap.aaf.auth.env.AuthzTrans, java.lang.String, int)
201      */
202     @Override
203     public List<Identity> getIDs(AuthzTrans trans, String user, int escalate) throws OrganizationException {
204         List<Identity> rv = new ArrayList<>();
205         int end = Math.min(3,Math.abs(escalate));
206         Identity id = null;
207         for(int i=0;i<end;++i) {
208             if(id==null) {
209                 id = getIdentity(trans,user);
210             } else {
211                 id = id.responsibleTo();
212             }
213             if(id==null) {
214                 break;
215             } else {
216                 rv.add(id);
217             }
218         }
219         return rv;
220     }
221
222     // Note: Return a null if found; return a String Message explaining why not found.
223     @Override
224     public String isValidID(final AuthzTrans trans, final String id) {
225         try {
226             DefaultOrgIdentity u = getIdentity(trans,id);
227             return (u==null||!u.isFound())?id + "is not an Identity in " + getName():null;
228         } catch (OrganizationException e) {
229             return getName() + " could not lookup " + id + ": " + e.getLocalizedMessage();
230         }
231     }
232     // Possible ID Pattern
233     //    private static final Pattern ID_PATTERN=Pattern.compile("([\\w.-]+@[\\w.-]+).{4-13}");
234     // Another one: ID_PATTERN = "(a-z[a-z0-9]{5-8}@.*).{4-13}";
235
236     @Override
237     public boolean isValidCred(final AuthzTrans trans, final String id) {
238         // have domain?
239         int at = id.indexOf('@');
240         String sid;
241         if (at > 0) {
242             // Use this to prevent passwords to any but THIS domain.
243 //            if (!id.regionMatches(at+1, domain, 0, id.length()-at-1)) {
244 //                return false;
245 //            }
246             sid = id.substring(0,at);
247         } else {
248             sid = id;
249         }
250         // We'll validate that it exists, rather than check patterns.
251
252         return isValidID(trans, sid)==null;
253         // Check Pattern (if checking existing is too long)
254         //        if (id.endsWith(SUFFIX) && ID_PATTERN.matcher(id).matches()) {
255         //            return true;
256         //        }
257         //        return false;
258     }
259
260     private static final String SPEC_CHARS = "!@#$%^*-+?/,:;.";
261     private static final Pattern PASS_PATTERN=Pattern.compile("(((?=.*[a-z,A-Z])(((?=.*\\d))|(?=.*[" + SPEC_CHARS +"]))).{6,20})");
262     /**
263      *  (                # Start of group
264      *  (?=.*[a-z,A-Z])    #   must contain one character
265      *
266      *  (?=.*\d)        #   must contain one digit from 0-9
267      *        OR
268      *  (?=.*[@#$%])    #   must contain one special symbols in the list SPEC_CHARS
269      *
270      *            .        #     match anything with previous condition checking
271      *          {6,20}    #        length at least 6 characters and maximum of 20
272      *  )                # End of group
273      *
274      * Another example, more stringent pattern
275      private static final Pattern PASS_PATTERN=Pattern.compile("((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[" + SPEC_CHARS +"]).{6,20})");
276      *  Attribution: from mkyong.com
277      *  (                # Start of group
278      *  (?=.*\d)        #   must contain one digit from 0-9
279      *  (?=.*[a-z])        #   must contain one lowercase characters
280      *  (?=.*[A-Z])        #   must contain one uppercase characters
281      *  (?=.*[@#$%])    #   must contain one special symbols in the list SPEC_CHARS
282      *            .        #     match anything with previous condition checking
283      *          {6,20}    #        length at least 6 characters and maximum of 20
284      *  )                # End of group
285      */
286     @Override
287     public String isValidPassword(final AuthzTrans trans, final String user, final String password, final String... prev) {
288         for (String p : prev) {
289             if (password.contains(p)) { // A more sophisticated algorithm might be better.
290                 return "Password too similar to previous passwords";
291             }
292         }
293         // If you have an Organization user/Password scheme, replace the following
294         if (PASS_PATTERN.matcher(password).matches()) {
295             return "";
296         }
297         return "Password does not match " + NAME + " Password Standards";
298     }
299
300     private static final String[] rules = new String[] {
301             "Passwords must contain letters",
302             "Passwords must contain one of the following:",
303             "  Number",
304             "  One special symbols in the list \""+ SPEC_CHARS + '"',
305             "Passwords must be between 6 and 20 chars in length",
306     };
307
308     @Override
309     public String[] getPasswordRules() {
310         return rules;
311     }
312
313     @Override
314     public Set<String> getIdentityTypes() {
315         return typeSet;
316     }
317
318     @Override
319     public Response notify(AuthzTrans trans, Notify type, String url, String[] identities, String[] ccs, String summary, Boolean urgent) {
320         String system = trans.getProperty("CASS_ENV", "");
321
322         ArrayList<String> toList = new ArrayList<>();
323         Identity identity;
324         if (identities != null) {
325             for (String user : identities) {
326                 try {
327                     identity = getIdentity(trans, user);
328                     if (identity == null) {
329                         trans.error().log(
330                                 "Failure to obtain User " + user + " for "
331                                         + getName());
332                     } else {
333                         toList.add(identity.email());
334                     }
335                 } catch (Exception e) {
336                     trans.error().log(
337                             e,
338                             "Failure to obtain User " + user + " for "
339                                     + getName());
340                 }
341             }
342         }
343
344         if (toList.isEmpty()) {
345             trans.error().log("No Users listed to email");
346             return Response.ERR_NotificationFailure;
347         }
348
349         ArrayList<String> ccList = new ArrayList<>();
350
351         // If we're sending an urgent email, CC the user's supervisor
352         //
353         if (urgent) {
354             trans.info().log("urgent msg for: " + identities[0]);
355             try {
356                 List<Identity> supervisors = getApprovers(trans, identities[0]);
357                 for (Identity us : supervisors) {
358                     trans.info().log("supervisor: " + us.email());
359                     ccList.add(us.email());
360                 }
361             } catch (Exception e) {
362                 trans.error().log(e,
363                         "Failed to find supervisor for  " + identities[0]);
364             }
365         }
366
367         if (ccs != null) {
368             for (String user : ccs) {
369                 try {
370                     identity = getIdentity(trans, user);
371                     ccList.add(identity.email());
372                 } catch (Exception e) {
373                     trans.error().log(
374                             e,
375                             "Failure to obtain User " + user + " for "
376                                     + getName());
377                 }
378             }
379         }
380
381         if (summary == null) {
382             summary = "";
383         }
384
385         switch (type) {
386         case Approval:
387             try {
388                 sendEmail(trans, toList, ccList,
389                         "AAF Approval Notification "
390                                 + (system.length() == 0 ? "" : "(ENV: "
391                                         + system + ")"),
392                         "AAF is the "
393                         + NAME
394                         + "System for Fine-Grained Authorizations.  You are being asked to Approve"
395                                 + (system.length() == 0 ? "" : " in the "
396                                         + system + " environment")
397                                 + " before AAF Actions can be taken.\n\n"
398                                 + "Please follow this link: \n\n\t" + url
399                                 + "\n\n" + summary, urgent);
400             } catch (Exception e) {
401
402                 trans.error().log(e, "Failure to send Email");
403                 return Response.ERR_NotificationFailure;
404             }
405             break;
406         case PasswordExpiration:
407             try {
408                 sendEmail(trans,
409                         toList,
410                         ccList,
411                         "AAF Password Expiration Warning "
412                                 + (system.length() == 0 ? "" : "(ENV: "
413                                         + system + ")"),
414                         "AAF is the "
415                         + NAME
416                         + " System for Authorizations.\n\nOne or more passwords will expire soon or have expired"
417                                 + (system.length() == 0 ? "" : " in the "
418                                         + system + " environment")
419                                 + ".\n\nPasswords expired for more than 30 days without action are subject to deletion.\n\n"
420                                 + "Please follow each link to add a New Password with Expiration Date. Either are valid until expiration. "
421                                 + "Use this time to change the passwords on your system. If issues, reply to this email.\n\n"
422                                 + summary, urgent);
423             } catch (Exception e) {
424                 trans.error().log(e, "Failure to send Email");
425                 return Response.ERR_NotificationFailure;
426             }
427             break;
428
429         case RoleExpiration:
430             try {
431                 sendEmail(
432                         trans,
433                         toList,
434                         ccList,
435                         "AAF Role Expiration Warning "
436                                 + (system.length() == 0 ? "" : "(ENV: "
437                                         + system + ")"),
438                         "AAF is the "
439                         + NAME
440                         + " System for Authorizations. One or more roles will expire soon"
441                                 + (system.length() == 0 ? "" : " in the "
442                                         + system + " environment")
443                                 + ".\n\nRoles expired for more than 30 days are subject to deletion."
444                                 + "Please follow this link the GUI Command line, and either 'extend' or 'del' the user in the role.\n"
445                                 + "If issues, reply to this email.\n\n\t" + url
446                                 + "\n\n" + summary, urgent);
447             } catch (Exception e) {
448                 trans.error().log(e, "Failure to send Email");
449                 return Response.ERR_NotificationFailure;
450             }
451             break;
452         default:
453             return Response.ERR_NotImplemented;
454         }
455         return Response.OK;
456     }
457
458
459     /**
460      * Default Policy is to set to 6 Months for Notification Types.
461      * add others/change as required
462      */
463     @Override
464     public Date whenToValidate(Notify type, Date lastValidated) {
465         switch(type) {
466             case Approval:
467             case PasswordExpiration:
468                 return null;
469             default:
470                 GregorianCalendar gc = new GregorianCalendar();
471                 gc.setTime(lastValidated);
472                 gc.add(GregorianCalendar.MONTH, 6);  // 6 month policy
473                 return gc.getTime();
474         }
475     }
476
477     @Override
478     public GregorianCalendar expiration(GregorianCalendar gc, Expiration exp, String... extra) {
479         GregorianCalendar now = new GregorianCalendar();
480         GregorianCalendar rv = gc==null?now:(GregorianCalendar)gc.clone();
481         switch (exp) {
482             case ExtendPassword:
483                 // Extending Password give 5 extra days, max 8 days from now
484                 rv.add(GregorianCalendar.DATE, 5);
485                 now.add(GregorianCalendar.DATE, 8);
486                 if (rv.after(now)) {
487                     rv = now;
488                 }
489                 break;
490             case Future:
491                 // Future requests last 15 days.
492                 now.add(GregorianCalendar.DATE, 15);
493                 rv = now;
494                 break;
495             case Password:
496                 // Passwords expire in 90 days
497                 now.add(GregorianCalendar.DATE, 90);
498                 rv = now;
499                 break;
500             case TempPassword:
501                 // Temporary Passwords last for 12 hours.
502                 now.add(GregorianCalendar.DATE, 90);
503                 rv = now;
504                 break;
505             case UserDelegate:
506                 // Delegations expire max in 2 months, renewable to 3
507                 rv.add(GregorianCalendar.MONTH, 2);
508                 now.add(GregorianCalendar.MONTH, 3);
509                 if (rv.after(now)) {
510                     rv = now;
511                 }
512                 break;
513             case UserInRole:
514                 // Roles expire in 6 months
515                 now.add(GregorianCalendar.MONTH, 6);
516                 rv = now;
517                 break;
518             case RevokedGracePeriodEnds:
519                 now.add(GregorianCalendar.DATE, 3);
520                 rv = now;
521                 break;
522             default:
523                 // Unless other wise set, 6 months is default
524                 now.add(GregorianCalendar.MONTH, 6);
525                 rv = now;
526                 break;
527         }
528         return rv;
529     }
530
531     @Override
532     public EmailWarnings emailWarningPolicy() {
533         return emailWarnings;
534     }
535
536     /**
537      * Assume the Supervisor is the Approver.
538      */
539     @Override
540     public List<Identity> getApprovers(AuthzTrans trans, String user) throws OrganizationException {
541         Identity orgIdentity = getIdentity(trans, user);
542         List<Identity> orgIdentitys = new ArrayList<>();
543         if (orgIdentity!=null) {
544             Identity supervisor = orgIdentity.responsibleTo();
545             if (supervisor!=null) {
546                 orgIdentitys.add(supervisor);
547             }
548         }
549         return orgIdentitys;
550     }
551
552     @Override
553     public String getApproverType() {
554         return "supervisor";
555     }
556
557     @Override
558     public int startOfDay() {
559         // TODO Auto-generated method stub
560         return 0;
561     }
562
563     @Override
564     public boolean canHaveMultipleCreds(String id) {
565         // External entities are likely mono-password... if you change it, it is a global change.
566         // This is great for people, but horrible for Applications.
567         //
568         // AAF's Password can have multiple Passwords, each with their own Expiration Date.
569         // For Default Org, we'll assume true for all, but when you add your external
570         // Identity stores, you need to return "false" if they cannot support multiple Passwords like AAF
571         return true;
572     }
573
574     @Override
575     public String validate(AuthzTrans trans, Policy policy, Executor executor, String... vars) throws OrganizationException {
576         String user;
577         switch(policy) {
578             case OWNS_MECHID:
579             case CREATE_MECHID:
580                 if (vars.length>0) {
581                     DefaultOrgIdentity thisID = getIdentity(trans,vars[0]);
582                     if ("a".equals(thisID.identity.status)) { // MechID
583                         DefaultOrgIdentity requestor = getIdentity(trans, trans.user());
584                         if (requestor!=null) {
585                             Identity mechid = getIdentity(trans, vars[0]);
586                             if (mechid!=null) {
587                                 Identity sponsor = mechid.responsibleTo();
588                                 if (sponsor!=null && requestor.fullID().equals(sponsor.fullID())) {
589                                     return null;
590                                 } else {
591                                     return trans.user() + " is not the Sponsor of MechID " + vars[0];
592                                 }
593                             }
594                         }
595                     }
596                 }
597                 return null;
598
599             case CREATE_MECHID_BY_PERM_ONLY:
600                 return getName() + " only allows sponsors to create MechIDs";
601
602             case MAY_EXTEND_CRED_EXPIRES:
603                 // If parm, use it, otherwise, trans
604                 user = vars.length>1?vars[1]:trans.user();
605                 return executor.hasPermission(user, root_ns,"password", root_ns , "extend")
606                         ?null:user + " does not have permission to extend passwords at " + getName();
607
608             default:
609                 return policy.name() + " is unsupported at " + getName();
610         }
611     }
612
613     @Override
614     public boolean isTestEnv() {
615         return false;
616     }
617
618     @Override
619     public void setTestMode(boolean dryRun) {
620         this.dryRun = dryRun;
621     }
622
623     private String extractRealm(final String r) {
624         int at;
625         if ((at=r.indexOf('@'))>=0) {
626             return FQI.reverseDomain(r.substring(at+1));
627         }
628         return r;
629     }
630     @Override
631     public boolean supportsRealm(final String r) {
632         if (r.endsWith(realm)) {
633             return true;
634         } else {
635             String erealm = extractRealm(r);
636             for (String sr : supportedRealms) {
637                 if (erealm.startsWith(sr)) {
638                     return true;
639                 }
640             }
641         }
642         return false;
643     }
644     
645         @Override
646         public String supportedDomain(String user) {
647                 if(user!=null) {
648                         int after_at = user.indexOf('@')+1;
649                         if(after_at<user.length()) {
650                                 String ud = FQI.reverseDomain(user);
651                                 if(ud.startsWith(getDomain())) {
652                                         return getDomain();
653                                 }
654                                 for(String s : supportedRealms) {
655                                         if(ud.startsWith(s)) {
656                                                 return FQI.reverseDomain(s);
657                                         }
658                                 }
659                         }
660                 }
661                 return null;
662         }
663
664     @Override
665     public synchronized void addSupportedRealm(final String r) {
666         supportedRealms.add(extractRealm(r));
667     }
668
669     @Override
670     public int sendEmail(AuthzTrans trans, List<String> toList, List<String> ccList, String subject, String body,
671             Boolean urgent) throws OrganizationException {
672         if (mailer!=null) {
673             String mailFrom = mailer.mailFrom();
674             List<String> to = new ArrayList<>();
675             for (String em : toList) {
676                 if (em.indexOf('@')<0) {
677                     to.add(new DefaultOrgIdentity(trans, em, this).email());
678                 } else {
679                     to.add(em);
680                 }
681             }
682
683             List<String> cc = new ArrayList<>();
684             if (ccList!=null) {
685                 if (!ccList.isEmpty()) {
686
687                     for (String em : ccList) {
688                         if (em.indexOf('@')<0) {
689                             cc.add(new DefaultOrgIdentity(trans, em, this).email());
690                         } else {
691                             cc.add(em);
692                         }
693                     }
694                 }
695
696                 // for now, I want all emails so we can see what goes out. Remove later
697                 if (!ccList.contains(mailFrom)) {
698                     ccList.add(mailFrom);
699                 }
700             }
701
702             return mailer.sendEmail(trans,dryRun?"DefaultOrg":null,to,cc,subject,body,urgent)?0:1;
703         } else {
704             return 0;
705         }
706     }
707 }