1/*
2 * Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package javax.security.auth.kerberos;
27
28import java.io.IOException;
29import java.io.ObjectInputStream;
30import java.io.ObjectOutputStream;
31import java.io.ObjectStreamField;
32import java.security.Permission;
33import java.security.PermissionCollection;
34import java.util.*;
35import java.util.concurrent.ConcurrentHashMap;
36
37/**
38 * This class is used to protect Kerberos services and the
39 * credentials necessary to access those services. There is a one to
40 * one mapping of a service principal and the credentials necessary
41 * to access the service. Therefore granting access to a service
42 * principal implicitly grants access to the credential necessary to
43 * establish a security context with the service principal. This
44 * applies regardless of whether the credentials are in a cache
45 * or acquired via an exchange with the KDC. The credential can
46 * be either a ticket granting ticket, a service ticket or a secret
47 * key from a key table.
48 * <p>
49 * A ServicePermission contains a service principal name and
50 * a list of actions which specify the context the credential can be
51 * used within.
52 * <p>
53 * The service principal name is the canonical name of the
54 * {@code KerberosPrincipal} supplying the service, that is
55 * the KerberosPrincipal represents a Kerberos service
56 * principal. This name is treated in a case sensitive manner.
57 * An asterisk may appear by itself, to signify any service principal.
58 * <p>
59 * Granting this permission implies that the caller can use a cached
60 * credential (TGT, service ticket or secret key) within the context
61 * designated by the action. In the case of the TGT, granting this
62 * permission also implies that the TGT can be obtained by an
63 * Authentication Service exchange.
64 * <p>
65 * Granting this permission also implies creating {@link KerberosPrincipal}
66 * or {@link org.ietf.jgss.GSSName GSSName} without providing a Kerberos
67 * realm, as long as the permission's service principal is in this realm.
68 * <p>
69 * The possible actions are:
70 *
71 * <pre>
72 *    initiate -              allow the caller to use the credential to
73 *                            initiate a security context with a service
74 *                            principal.
75 *
76 *    accept -                allow the caller to use the credential to
77 *                            accept security context as a particular
78 *                            principal.
79 * </pre>
80 *
81 * For example, to specify the permission to access to the TGT to
82 * initiate a security context the permission is constructed as follows:
83 *
84 * <pre>
85 *     ServicePermission("krbtgt/EXAMPLE.COM@EXAMPLE.COM", "initiate");
86 * </pre>
87 * <p>
88 * To obtain a service ticket to initiate a context with the "host"
89 * service the permission is constructed as follows:
90 * <pre>
91 *     ServicePermission("host/foo.example.com@EXAMPLE.COM", "initiate");
92 * </pre>
93 * <p>
94 * For a Kerberized server the action is "accept". For example, the permission
95 * necessary to access and use the secret key of the  Kerberized "host"
96 * service (telnet and the likes)  would be constructed as follows:
97 *
98 * <pre>
99 *     ServicePermission("host/foo.example.com@EXAMPLE.COM", "accept");
100 * </pre>
101 *
102 * @since 1.4
103 */
104
105public final class ServicePermission extends Permission
106    implements java.io.Serializable {
107
108    private static final long serialVersionUID = -1227585031618624935L;
109
110    /**
111     * Initiate a security context to the specified service
112     */
113    private final static int INITIATE   = 0x1;
114
115    /**
116     * Accept a security context
117     */
118    private final static int ACCEPT     = 0x2;
119
120    /**
121     * All actions
122     */
123    private final static int ALL        = INITIATE|ACCEPT;
124
125    /**
126     * No actions.
127     */
128    private final static int NONE    = 0x0;
129
130    // the actions mask
131    private transient int mask;
132
133    /**
134     * the actions string.
135     *
136     * @serial
137     */
138
139    private String actions; // Left null as long as possible, then
140                            // created and re-used in the getAction function.
141
142    /**
143     * Create a new {@code ServicePermission}
144     * with the specified {@code servicePrincipal}
145     * and {@code action}.
146     *
147     * @param servicePrincipal the name of the service principal.
148     * An asterisk may appear by itself, to signify any service principal.
149     *
150     * @param action the action string
151     */
152    public ServicePermission(String servicePrincipal, String action) {
153        // Note: servicePrincipal can be "@REALM" which means any principal in
154        // this realm implies it. action can be "-" which means any
155        // action implies it.
156        super(servicePrincipal);
157        init(servicePrincipal, getMask(action));
158    }
159
160    /**
161     * Creates a ServicePermission object with the specified servicePrincipal
162     * and a pre-calculated mask. Avoids the overhead of re-computing the mask.
163     * Called by ServicePermissionCollection.
164     */
165    ServicePermission(String servicePrincipal, int mask) {
166        super(servicePrincipal);
167        init(servicePrincipal, mask);
168    }
169
170    /**
171     * Initialize the ServicePermission object.
172     */
173    private void init(String servicePrincipal, int mask) {
174
175        if (servicePrincipal == null)
176                throw new NullPointerException("service principal can't be null");
177
178        if ((mask & ALL) != mask)
179            throw new IllegalArgumentException("invalid actions mask");
180
181        this.mask = mask;
182    }
183
184
185    /**
186     * Checks if this Kerberos service permission object "implies" the
187     * specified permission.
188     * <P>
189     * More specifically, this method returns true if all of the following
190     * are true (and returns false if any of them are not):
191     * <ul>
192     * <li> <i>p</i> is an instanceof {@code ServicePermission},
193     * <li> <i>p</i>'s actions are a proper subset of this
194     * {@code ServicePermission}'s actions,
195     * <li> <i>p</i>'s name is equal to this {@code ServicePermission}'s name
196     * or this {@code ServicePermission}'s name is "*".
197     * </ul>
198     *
199     * @param p the permission to check against.
200     *
201     * @return true if the specified permission is implied by this object,
202     * false if not.
203     */
204    @Override
205    public boolean implies(Permission p) {
206        if (!(p instanceof ServicePermission))
207            return false;
208
209        ServicePermission that = (ServicePermission) p;
210
211        return ((this.mask & that.mask) == that.mask) &&
212            impliesIgnoreMask(that);
213    }
214
215
216    boolean impliesIgnoreMask(ServicePermission p) {
217        return ((this.getName().equals("*")) ||
218                this.getName().equals(p.getName()) ||
219                (p.getName().startsWith("@") &&
220                        this.getName().endsWith(p.getName())));
221    }
222
223    /**
224     * Checks two ServicePermission objects for equality.
225     *
226     * @param obj the object to test for equality with this object.
227     *
228     * @return true if {@code obj} is a ServicePermission, and has the
229     *  same service principal, and actions as this
230     * ServicePermission object.
231     */
232    @Override
233    public boolean equals(Object obj) {
234        if (obj == this)
235            return true;
236
237        if (! (obj instanceof ServicePermission))
238            return false;
239
240        ServicePermission that = (ServicePermission) obj;
241        return ((this.mask & that.mask) == that.mask) &&
242            this.getName().equals(that.getName());
243
244
245    }
246
247    /**
248     * Returns the hash code value for this object.
249     *
250     * @return a hash code value for this object.
251     */
252    @Override
253    public int hashCode() {
254        return (getName().hashCode() ^ mask);
255    }
256
257
258    /**
259     * Returns the "canonical string representation" of the actions in the
260     * specified mask.
261     * Always returns present actions in the following order:
262     * initiate, accept.
263     *
264     * @param mask a specific integer action mask to translate into a string
265     * @return the canonical string representation of the actions
266     */
267    static String getActions(int mask)
268    {
269        StringBuilder sb = new StringBuilder();
270        boolean comma = false;
271
272        if ((mask & INITIATE) == INITIATE) {
273            if (comma) sb.append(',');
274            else comma = true;
275            sb.append("initiate");
276        }
277
278        if ((mask & ACCEPT) == ACCEPT) {
279            if (comma) sb.append(',');
280            else comma = true;
281            sb.append("accept");
282        }
283
284        return sb.toString();
285    }
286
287    /**
288     * Returns the canonical string representation of the actions.
289     * Always returns present actions in the following order:
290     * initiate, accept.
291     */
292    @Override
293    public String getActions() {
294        if (actions == null)
295            actions = getActions(this.mask);
296
297        return actions;
298    }
299
300
301    /**
302     * Returns a PermissionCollection object for storing
303     * ServicePermission objects.
304     * <br>
305     * ServicePermission objects must be stored in a manner that
306     * allows them to be inserted into the collection in any order, but
307     * that also enables the PermissionCollection implies method to
308     * be implemented in an efficient (and consistent) manner.
309     *
310     * @return a new PermissionCollection object suitable for storing
311     * ServicePermissions.
312     */
313    @Override
314    public PermissionCollection newPermissionCollection() {
315        return new KrbServicePermissionCollection();
316    }
317
318    /**
319     * Return the current action mask.
320     *
321     * @return the actions mask.
322     */
323    int getMask() {
324        return mask;
325    }
326
327    /**
328     * Convert an action string to an integer actions mask.
329     *
330     * Note: if action is "-", action will be NONE, which means any
331     * action implies it.
332     *
333     * @param action the action string.
334     * @return the action mask
335     */
336    private static int getMask(String action) {
337
338        if (action == null) {
339            throw new NullPointerException("action can't be null");
340        }
341
342        if (action.equals("")) {
343            throw new IllegalArgumentException("action can't be empty");
344        }
345
346        int mask = NONE;
347
348        char[] a = action.toCharArray();
349
350        if (a.length == 1 && a[0] == '-') {
351            return mask;
352        }
353
354        int i = a.length - 1;
355
356        while (i != -1) {
357            char c;
358
359            // skip whitespace
360            while ((i!=-1) && ((c = a[i]) == ' ' ||
361                               c == '\r' ||
362                               c == '\n' ||
363                               c == '\f' ||
364                               c == '\t'))
365                i--;
366
367            // check for the known strings
368            int matchlen;
369
370            if (i >= 7 && (a[i-7] == 'i' || a[i-7] == 'I') &&
371                          (a[i-6] == 'n' || a[i-6] == 'N') &&
372                          (a[i-5] == 'i' || a[i-5] == 'I') &&
373                          (a[i-4] == 't' || a[i-4] == 'T') &&
374                          (a[i-3] == 'i' || a[i-3] == 'I') &&
375                          (a[i-2] == 'a' || a[i-2] == 'A') &&
376                          (a[i-1] == 't' || a[i-1] == 'T') &&
377                          (a[i] == 'e' || a[i] == 'E'))
378            {
379                matchlen = 8;
380                mask |= INITIATE;
381
382            } else if (i >= 5 && (a[i-5] == 'a' || a[i-5] == 'A') &&
383                                 (a[i-4] == 'c' || a[i-4] == 'C') &&
384                                 (a[i-3] == 'c' || a[i-3] == 'C') &&
385                                 (a[i-2] == 'e' || a[i-2] == 'E') &&
386                                 (a[i-1] == 'p' || a[i-1] == 'P') &&
387                                 (a[i] == 't' || a[i] == 'T'))
388            {
389                matchlen = 6;
390                mask |= ACCEPT;
391
392            } else {
393                // parse error
394                throw new IllegalArgumentException(
395                        "invalid permission: " + action);
396            }
397
398            // make sure we didn't just match the tail of a word
399            // like "ackbarfaccept".  Also, skip to the comma.
400            boolean seencomma = false;
401            while (i >= matchlen && !seencomma) {
402                switch(a[i-matchlen]) {
403                case ',':
404                    seencomma = true;
405                    break;
406                case ' ': case '\r': case '\n':
407                case '\f': case '\t':
408                    break;
409                default:
410                    throw new IllegalArgumentException(
411                            "invalid permission: " + action);
412                }
413                i--;
414            }
415
416            // point i at the location of the comma minus one (or -1).
417            i -= matchlen;
418        }
419
420        return mask;
421    }
422
423
424    /**
425     * WriteObject is called to save the state of the ServicePermission
426     * to a stream. The actions are serialized, and the superclass
427     * takes care of the name.
428     */
429    private void writeObject(java.io.ObjectOutputStream s)
430        throws IOException
431    {
432        // Write out the actions. The superclass takes care of the name
433        // call getActions to make sure actions field is initialized
434        if (actions == null)
435            getActions();
436        s.defaultWriteObject();
437    }
438
439    /**
440     * readObject is called to restore the state of the
441     * ServicePermission from a stream.
442     */
443    private void readObject(java.io.ObjectInputStream s)
444         throws IOException, ClassNotFoundException
445    {
446        // Read in the action, then initialize the rest
447        s.defaultReadObject();
448        init(getName(),getMask(actions));
449    }
450
451
452    /*
453      public static void main(String[] args) throws Exception {
454      ServicePermission this_ =
455      new ServicePermission(args[0], "accept");
456      ServicePermission that_ =
457      new ServicePermission(args[1], "accept,initiate");
458      System.out.println("-----\n");
459      System.out.println("this.implies(that) = " + this_.implies(that_));
460      System.out.println("-----\n");
461      System.out.println("this = "+this_);
462      System.out.println("-----\n");
463      System.out.println("that = "+that_);
464      System.out.println("-----\n");
465
466      KrbServicePermissionCollection nps =
467      new KrbServicePermissionCollection();
468      nps.add(this_);
469      nps.add(new ServicePermission("nfs/example.com@EXAMPLE.COM",
470      "accept"));
471      nps.add(new ServicePermission("host/example.com@EXAMPLE.COM",
472      "initiate"));
473      System.out.println("nps.implies(that) = " + nps.implies(that_));
474      System.out.println("-----\n");
475
476      Enumeration e = nps.elements();
477
478      while (e.hasMoreElements()) {
479      ServicePermission x =
480      (ServicePermission) e.nextElement();
481      System.out.println("nps.e = " + x);
482      }
483
484      }
485    */
486
487}
488
489
490final class KrbServicePermissionCollection extends PermissionCollection
491    implements java.io.Serializable {
492
493    // Key is the service principal, value is the ServicePermission.
494    // Not serialized; see serialization section at end of class
495    private transient ConcurrentHashMap<String, Permission> perms;
496
497    public KrbServicePermissionCollection() {
498        perms = new ConcurrentHashMap<>();
499    }
500
501    /**
502     * Check and see if this collection of permissions implies the permissions
503     * expressed in "permission".
504     *
505     * @param permission the Permission object to compare
506     *
507     * @return true if "permission" is a proper subset of a permission in
508     * the collection, false if not.
509     */
510    @Override
511    public boolean implies(Permission permission) {
512        if (! (permission instanceof ServicePermission))
513            return false;
514
515        ServicePermission np = (ServicePermission) permission;
516        int desired = np.getMask();
517
518        if (desired == 0) {
519            for (Permission p: perms.values()) {
520                ServicePermission sp = (ServicePermission)p;
521                if (sp.impliesIgnoreMask(np)) {
522                    return true;
523                }
524            }
525            return false;
526        }
527
528
529        // first, check for wildcard principal
530        ServicePermission x = (ServicePermission)perms.get("*");
531        if (x != null) {
532            if ((x.getMask() & desired) == desired) {
533                return true;
534            }
535        }
536
537        // otherwise, check for match on principal
538        x = (ServicePermission)perms.get(np.getName());
539        if (x != null) {
540            //System.out.println("  trying "+x);
541            if ((x.getMask() & desired) == desired) {
542                return true;
543            }
544        }
545        return false;
546    }
547
548    /**
549     * Adds a permission to the ServicePermissions. The key for
550     * the hash is the name.
551     *
552     * @param permission the Permission object to add.
553     *
554     * @exception IllegalArgumentException - if the permission is not a
555     *                                       ServicePermission
556     *
557     * @exception SecurityException - if this PermissionCollection object
558     *                                has been marked readonly
559     */
560    @Override
561    public void add(Permission permission) {
562        if (! (permission instanceof ServicePermission))
563            throw new IllegalArgumentException("invalid permission: "+
564                                               permission);
565        if (isReadOnly())
566            throw new SecurityException("attempt to add a Permission to a readonly PermissionCollection");
567
568        ServicePermission sp = (ServicePermission)permission;
569        String princName = sp.getName();
570
571        // Add permission to map if it is absent, or replace with new
572        // permission if applicable. NOTE: cannot use lambda for
573        // remappingFunction parameter until JDK-8076596 is fixed.
574        perms.merge(princName, sp,
575            new java.util.function.BiFunction<>() {
576                @Override
577                public Permission apply(Permission existingVal,
578                                        Permission newVal) {
579                    int oldMask = ((ServicePermission)existingVal).getMask();
580                    int newMask = ((ServicePermission)newVal).getMask();
581                    if (oldMask != newMask) {
582                        int effective = oldMask | newMask;
583                        if (effective == newMask) {
584                            return newVal;
585                        }
586                        if (effective != oldMask) {
587                            return new ServicePermission(princName, effective);
588                        }
589                    }
590                    return existingVal;
591                }
592            }
593        );
594    }
595
596    /**
597     * Returns an enumeration of all the ServicePermission objects
598     * in the container.
599     *
600     * @return an enumeration of all the ServicePermission objects.
601     */
602    @Override
603    public Enumeration<Permission> elements() {
604        return perms.elements();
605    }
606
607    private static final long serialVersionUID = -4118834211490102011L;
608
609    // Need to maintain serialization interoperability with earlier releases,
610    // which had the serializable field:
611    // private Vector permissions;
612
613    /**
614     * @serialField permissions java.util.Vector
615     *     A list of ServicePermission objects.
616     */
617    private static final ObjectStreamField[] serialPersistentFields = {
618        new ObjectStreamField("permissions", Vector.class),
619    };
620
621    /**
622     * @serialData "permissions" field (a Vector containing the ServicePermissions).
623     */
624    /*
625     * Writes the contents of the perms field out as a Vector for
626     * serialization compatibility with earlier releases.
627     */
628    private void writeObject(ObjectOutputStream out) throws IOException {
629        // Don't call out.defaultWriteObject()
630
631        // Write out Vector
632        Vector<Permission> permissions = new Vector<>(perms.values());
633
634        ObjectOutputStream.PutField pfields = out.putFields();
635        pfields.put("permissions", permissions);
636        out.writeFields();
637    }
638
639    /*
640     * Reads in a Vector of ServicePermissions and saves them in the perms field.
641     */
642    @SuppressWarnings("unchecked")
643    private void readObject(ObjectInputStream in)
644        throws IOException, ClassNotFoundException
645    {
646        // Don't call defaultReadObject()
647
648        // Read in serialized fields
649        ObjectInputStream.GetField gfields = in.readFields();
650
651        // Get the one we want
652        Vector<Permission> permissions =
653                (Vector<Permission>)gfields.get("permissions", null);
654        perms = new ConcurrentHashMap<>(permissions.size());
655        for (Permission perm : permissions) {
656            perms.put(perm.getName(), perm);
657        }
658    }
659}
660