ServicePermission.java revision 12318:bee34b1dcbf1
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 KereberosPrincipal} 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 * The possible actions are:
66 *
67 * <pre>
68 *    initiate -              allow the caller to use the credential to
69 *                            initiate a security context with a service
70 *                            principal.
71 *
72 *    accept -                allow the caller to use the credential to
73 *                            accept security context as a particular
74 *                            principal.
75 * </pre>
76 *
77 * For example, to specify the permission to access to the TGT to
78 * initiate a security context the permission is constructed as follows:
79 *
80 * <pre>
81 *     ServicePermission("krbtgt/EXAMPLE.COM@EXAMPLE.COM", "initiate");
82 * </pre>
83 * <p>
84 * To obtain a service ticket to initiate a context with the "host"
85 * service the permission is constructed as follows:
86 * <pre>
87 *     ServicePermission("host/foo.example.com@EXAMPLE.COM", "initiate");
88 * </pre>
89 * <p>
90 * For a Kerberized server the action is "accept". For example, the permission
91 * necessary to access and use the secret key of the  Kerberized "host"
92 * service (telnet and the likes)  would be constructed as follows:
93 *
94 * <pre>
95 *     ServicePermission("host/foo.example.com@EXAMPLE.COM", "accept");
96 * </pre>
97 *
98 * @since 1.4
99 */
100
101public final class ServicePermission extends Permission
102    implements java.io.Serializable {
103
104    private static final long serialVersionUID = -1227585031618624935L;
105
106    /**
107     * Initiate a security context to the specified service
108     */
109    private final static int INITIATE   = 0x1;
110
111    /**
112     * Accept a security context
113     */
114    private final static int ACCEPT     = 0x2;
115
116    /**
117     * All actions
118     */
119    private final static int ALL        = INITIATE|ACCEPT;
120
121    /**
122     * No actions.
123     */
124    private final static int NONE    = 0x0;
125
126    // the actions mask
127    private transient int mask;
128
129    /**
130     * the actions string.
131     *
132     * @serial
133     */
134
135    private String actions; // Left null as long as possible, then
136                            // created and re-used in the getAction function.
137
138    /**
139     * Create a new {@code ServicePermission}
140     * with the specified {@code servicePrincipal}
141     * and {@code action}.
142     *
143     * @param servicePrincipal the name of the service principal.
144     * An asterisk may appear by itself, to signify any service principal.
145     *
146     * @param action the action string
147     */
148    public ServicePermission(String servicePrincipal, String action) {
149        super(servicePrincipal);
150        init(servicePrincipal, getMask(action));
151    }
152
153    /**
154     * Creates a ServicePermission object with the specified servicePrincipal
155     * and a pre-calculated mask. Avoids the overhead of re-computing the mask.
156     * Called by ServicePermissionCollection.
157     */
158    ServicePermission(String servicePrincipal, int mask) {
159        super(servicePrincipal);
160        init(servicePrincipal, mask);
161    }
162
163    /**
164     * Initialize the ServicePermission object.
165     */
166    private void init(String servicePrincipal, int mask) {
167
168        if (servicePrincipal == null)
169                throw new NullPointerException("service principal can't be null");
170
171        if ((mask & ALL) != mask)
172            throw new IllegalArgumentException("invalid actions mask");
173
174        this.mask = mask;
175    }
176
177
178    /**
179     * Checks if this Kerberos service permission object "implies" the
180     * specified permission.
181     * <P>
182     * If none of the above are true, {@code implies} returns false.
183     * @param p the permission to check against.
184     *
185     * @return true if the specified permission is implied by this object,
186     * false if not.
187     */
188    @Override
189    public boolean implies(Permission p) {
190        if (!(p instanceof ServicePermission))
191            return false;
192
193        ServicePermission that = (ServicePermission) p;
194
195        return ((this.mask & that.mask) == that.mask) &&
196            impliesIgnoreMask(that);
197    }
198
199
200    boolean impliesIgnoreMask(ServicePermission p) {
201        return ((this.getName().equals("*")) ||
202                this.getName().equals(p.getName()));
203    }
204
205    /**
206     * Checks two ServicePermission objects for equality.
207     *
208     * @param obj the object to test for equality with this object.
209     *
210     * @return true if {@code obj} is a ServicePermission, and has the
211     *  same service principal, and actions as this
212     * ServicePermission object.
213     */
214    @Override
215    public boolean equals(Object obj) {
216        if (obj == this)
217            return true;
218
219        if (! (obj instanceof ServicePermission))
220            return false;
221
222        ServicePermission that = (ServicePermission) obj;
223        return ((this.mask & that.mask) == that.mask) &&
224            this.getName().equals(that.getName());
225
226
227    }
228
229    /**
230     * Returns the hash code value for this object.
231     *
232     * @return a hash code value for this object.
233     */
234    @Override
235    public int hashCode() {
236        return (getName().hashCode() ^ mask);
237    }
238
239
240    /**
241     * Returns the "canonical string representation" of the actions in the
242     * specified mask.
243     * Always returns present actions in the following order:
244     * initiate, accept.
245     *
246     * @param mask a specific integer action mask to translate into a string
247     * @return the canonical string representation of the actions
248     */
249    static String getActions(int mask)
250    {
251        StringBuilder sb = new StringBuilder();
252        boolean comma = false;
253
254        if ((mask & INITIATE) == INITIATE) {
255            if (comma) sb.append(',');
256            else comma = true;
257            sb.append("initiate");
258        }
259
260        if ((mask & ACCEPT) == ACCEPT) {
261            if (comma) sb.append(',');
262            else comma = true;
263            sb.append("accept");
264        }
265
266        return sb.toString();
267    }
268
269    /**
270     * Returns the canonical string representation of the actions.
271     * Always returns present actions in the following order:
272     * initiate, accept.
273     */
274    @Override
275    public String getActions() {
276        if (actions == null)
277            actions = getActions(this.mask);
278
279        return actions;
280    }
281
282
283    /**
284     * Returns a PermissionCollection object for storing
285     * ServicePermission objects.
286     * <br>
287     * ServicePermission objects must be stored in a manner that
288     * allows them to be inserted into the collection in any order, but
289     * that also enables the PermissionCollection implies method to
290     * be implemented in an efficient (and consistent) manner.
291     *
292     * @return a new PermissionCollection object suitable for storing
293     * ServicePermissions.
294     */
295    @Override
296    public PermissionCollection newPermissionCollection() {
297        return new KrbServicePermissionCollection();
298    }
299
300    /**
301     * Return the current action mask.
302     *
303     * @return the actions mask.
304     */
305    int getMask() {
306        return mask;
307    }
308
309    /**
310     * Convert an action string to an integer actions mask.
311     *
312     * @param action the action string
313     * @return the action mask
314     */
315    private static int getMask(String action) {
316
317        if (action == null) {
318            throw new NullPointerException("action can't be null");
319        }
320
321        if (action.equals("")) {
322            throw new IllegalArgumentException("action can't be empty");
323        }
324
325        int mask = NONE;
326
327        char[] a = action.toCharArray();
328
329        int i = a.length - 1;
330        if (i < 0)
331            return mask;
332
333        while (i != -1) {
334            char c;
335
336            // skip whitespace
337            while ((i!=-1) && ((c = a[i]) == ' ' ||
338                               c == '\r' ||
339                               c == '\n' ||
340                               c == '\f' ||
341                               c == '\t'))
342                i--;
343
344            // check for the known strings
345            int matchlen;
346
347            if (i >= 7 && (a[i-7] == 'i' || a[i-7] == 'I') &&
348                          (a[i-6] == 'n' || a[i-6] == 'N') &&
349                          (a[i-5] == 'i' || a[i-5] == 'I') &&
350                          (a[i-4] == 't' || a[i-4] == 'T') &&
351                          (a[i-3] == 'i' || a[i-3] == 'I') &&
352                          (a[i-2] == 'a' || a[i-2] == 'A') &&
353                          (a[i-1] == 't' || a[i-1] == 'T') &&
354                          (a[i] == 'e' || a[i] == 'E'))
355            {
356                matchlen = 8;
357                mask |= INITIATE;
358
359            } else if (i >= 5 && (a[i-5] == 'a' || a[i-5] == 'A') &&
360                                 (a[i-4] == 'c' || a[i-4] == 'C') &&
361                                 (a[i-3] == 'c' || a[i-3] == 'C') &&
362                                 (a[i-2] == 'e' || a[i-2] == 'E') &&
363                                 (a[i-1] == 'p' || a[i-1] == 'P') &&
364                                 (a[i] == 't' || a[i] == 'T'))
365            {
366                matchlen = 6;
367                mask |= ACCEPT;
368
369            } else {
370                // parse error
371                throw new IllegalArgumentException(
372                        "invalid permission: " + action);
373            }
374
375            // make sure we didn't just match the tail of a word
376            // like "ackbarfaccept".  Also, skip to the comma.
377            boolean seencomma = false;
378            while (i >= matchlen && !seencomma) {
379                switch(a[i-matchlen]) {
380                case ',':
381                    seencomma = true;
382                    break;
383                case ' ': case '\r': case '\n':
384                case '\f': case '\t':
385                    break;
386                default:
387                    throw new IllegalArgumentException(
388                            "invalid permission: " + action);
389                }
390                i--;
391            }
392
393            // point i at the location of the comma minus one (or -1).
394            i -= matchlen;
395        }
396
397        return mask;
398    }
399
400
401    /**
402     * WriteObject is called to save the state of the ServicePermission
403     * to a stream. The actions are serialized, and the superclass
404     * takes care of the name.
405     */
406    private void writeObject(java.io.ObjectOutputStream s)
407        throws IOException
408    {
409        // Write out the actions. The superclass takes care of the name
410        // call getActions to make sure actions field is initialized
411        if (actions == null)
412            getActions();
413        s.defaultWriteObject();
414    }
415
416    /**
417     * readObject is called to restore the state of the
418     * ServicePermission from a stream.
419     */
420    private void readObject(java.io.ObjectInputStream s)
421         throws IOException, ClassNotFoundException
422    {
423        // Read in the action, then initialize the rest
424        s.defaultReadObject();
425        init(getName(),getMask(actions));
426    }
427
428
429    /*
430      public static void main(String[] args) throws Exception {
431      ServicePermission this_ =
432      new ServicePermission(args[0], "accept");
433      ServicePermission that_ =
434      new ServicePermission(args[1], "accept,initiate");
435      System.out.println("-----\n");
436      System.out.println("this.implies(that) = " + this_.implies(that_));
437      System.out.println("-----\n");
438      System.out.println("this = "+this_);
439      System.out.println("-----\n");
440      System.out.println("that = "+that_);
441      System.out.println("-----\n");
442
443      KrbServicePermissionCollection nps =
444      new KrbServicePermissionCollection();
445      nps.add(this_);
446      nps.add(new ServicePermission("nfs/example.com@EXAMPLE.COM",
447      "accept"));
448      nps.add(new ServicePermission("host/example.com@EXAMPLE.COM",
449      "initiate"));
450      System.out.println("nps.implies(that) = " + nps.implies(that_));
451      System.out.println("-----\n");
452
453      Enumeration e = nps.elements();
454
455      while (e.hasMoreElements()) {
456      ServicePermission x =
457      (ServicePermission) e.nextElement();
458      System.out.println("nps.e = " + x);
459      }
460
461      }
462    */
463
464}
465
466
467final class KrbServicePermissionCollection extends PermissionCollection
468    implements java.io.Serializable {
469
470    // Key is the service principal, value is the ServicePermission.
471    // Not serialized; see serialization section at end of class
472    private transient ConcurrentHashMap<String, Permission> perms;
473
474    public KrbServicePermissionCollection() {
475        perms = new ConcurrentHashMap<>();
476    }
477
478    /**
479     * Check and see if this collection of permissions implies the permissions
480     * expressed in "permission".
481     *
482     * @param permission the Permission object to compare
483     *
484     * @return true if "permission" is a proper subset of a permission in
485     * the collection, false if not.
486     */
487    @Override
488    public boolean implies(Permission permission) {
489        if (! (permission instanceof ServicePermission))
490            return false;
491
492        ServicePermission np = (ServicePermission) permission;
493        int desired = np.getMask();
494
495        // first, check for wildcard principal
496        ServicePermission x = (ServicePermission)perms.get("*");
497        if (x != null) {
498            if ((x.getMask() & desired) == desired) {
499                return true;
500            }
501        }
502
503        // otherwise, check for match on principal
504        x = (ServicePermission)perms.get(np.getName());
505        if (x != null) {
506            //System.out.println("  trying "+x);
507            if ((x.getMask() & desired) == desired) {
508                return true;
509            }
510        }
511        return false;
512    }
513
514    /**
515     * Adds a permission to the ServicePermissions. The key for
516     * the hash is the name.
517     *
518     * @param permission the Permission object to add.
519     *
520     * @exception IllegalArgumentException - if the permission is not a
521     *                                       ServicePermission
522     *
523     * @exception SecurityException - if this PermissionCollection object
524     *                                has been marked readonly
525     */
526    @Override
527    public void add(Permission permission) {
528        if (! (permission instanceof ServicePermission))
529            throw new IllegalArgumentException("invalid permission: "+
530                                               permission);
531        if (isReadOnly())
532            throw new SecurityException("attempt to add a Permission to a readonly PermissionCollection");
533
534        ServicePermission sp = (ServicePermission)permission;
535        String princName = sp.getName();
536
537        // Add permission to map if it is absent, or replace with new
538        // permission if applicable. NOTE: cannot use lambda for
539        // remappingFunction parameter until JDK-8076596 is fixed.
540        perms.merge(princName, sp,
541            new java.util.function.BiFunction<>() {
542                @Override
543                public Permission apply(Permission existingVal,
544                                        Permission newVal) {
545                    int oldMask = ((ServicePermission)existingVal).getMask();
546                    int newMask = ((ServicePermission)newVal).getMask();
547                    if (oldMask != newMask) {
548                        int effective = oldMask | newMask;
549                        if (effective == newMask) {
550                            return newVal;
551                        }
552                        if (effective != oldMask) {
553                            return new ServicePermission(princName, effective);
554                        }
555                    }
556                    return existingVal;
557                }
558            }
559        );
560    }
561
562    /**
563     * Returns an enumeration of all the ServicePermission objects
564     * in the container.
565     *
566     * @return an enumeration of all the ServicePermission objects.
567     */
568    @Override
569    public Enumeration<Permission> elements() {
570        return perms.elements();
571    }
572
573    private static final long serialVersionUID = -4118834211490102011L;
574
575    // Need to maintain serialization interoperability with earlier releases,
576    // which had the serializable field:
577    // private Vector permissions;
578
579    /**
580     * @serialField permissions java.util.Vector
581     *     A list of ServicePermission objects.
582     */
583    private static final ObjectStreamField[] serialPersistentFields = {
584        new ObjectStreamField("permissions", Vector.class),
585    };
586
587    /**
588     * @serialData "permissions" field (a Vector containing the ServicePermissions).
589     */
590    /*
591     * Writes the contents of the perms field out as a Vector for
592     * serialization compatibility with earlier releases.
593     */
594    private void writeObject(ObjectOutputStream out) throws IOException {
595        // Don't call out.defaultWriteObject()
596
597        // Write out Vector
598        Vector<Permission> permissions = new Vector<>(perms.values());
599
600        ObjectOutputStream.PutField pfields = out.putFields();
601        pfields.put("permissions", permissions);
602        out.writeFields();
603    }
604
605    /*
606     * Reads in a Vector of ServicePermissions and saves them in the perms field.
607     */
608    @SuppressWarnings("unchecked")
609    private void readObject(ObjectInputStream in)
610        throws IOException, ClassNotFoundException
611    {
612        // Don't call defaultReadObject()
613
614        // Read in serialized fields
615        ObjectInputStream.GetField gfields = in.readFields();
616
617        // Get the one we want
618        Vector<Permission> permissions =
619                (Vector<Permission>)gfields.get("permissions", null);
620        perms = new ConcurrentHashMap<>(permissions.size());
621        for (Permission perm : permissions) {
622            perms.put(perm.getName(), perm);
623        }
624    }
625}
626