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