|
Once a class has been loaded into the virtual machine by a class loader or by the default class loading mechanism and checked by the verifier, the third security mechanism of the Java platform springs into action: the security manager. A security manager is a class that controls whether a specific operation is permitted. Operations checked by a security manager include the following:
There are many other checks such as these throughout the Java library. The default behavior when running Java applications is that no security manager is installed, so all these operations are permitted. The applet viewer, on the other hand, immediately installs a security manager that is quite restrictive. For example, applets are not allowed to exit the virtual machine. If they try calling the exit method, then a security exception is thrown. Here is what happens in detail. The exit method of the Runtime class calls the checkExit method of the security manager. Here is the entire code of the exit method: public void exit(int status) { SecurityManager security = System.getSecurityManager(); if (security != null) security.checkExit(status); exitInternal(status); } The security manager now checks if the exit request came from the browser or an individual applet. If the security manager agrees with the exit request, then the checkExit method simply returns and normal processing continues. However, if the security manager doesn't want to grant the request, the checkExit method throws a SecurityException. The exit method continues only if no exception occurred. It then calls the private native exitInternal method that actually terminates the virtual machine. There is no other way of terminating the virtual machine, and because the exitInternal method is private, it cannot be called from any other class. Thus, any code that attempts to exit the virtual machine must go through the exit method and thus through the checkExit security check without triggering a security exception. Clearly, the integrity of the security policy depends on careful coding. The providers of system services in the standard library must be careful to always consult the security manager before attempting any sensitive operation. When you run a Java application, the default is that no security manager is running. Your program can install a specific security manager by a call to the static setSecurityManager method in the System class. Once your program installs a security manager, any attempt to install a second security manager succeeds only if the first security manager agrees to be replaced. This is clearly essential; otherwise, a bad applet could install its own security manager. Thus, although it is possible to have multiple class loaders, a program in the Java programming language can be governed by only one security manager. It is up to the implementor of that security manager to decide whether to grant all classes the same access or whether to take the origins of the classes into account before deciding what to do. The default security manager of the Java 2 platform allows both programmers and system administrators fine-grained control over individual security permissions. We describe these features in the following section. First, we summarize the Java 2 platform security model. We then show how you can control permissions with policy files. Finally, we explain how you can define your own permission types and how you can extend the default security manager class. Java 2 Platform SecurityJDK 1.0 had a very simple security model: Local classes had full permissions, and remote classes were confined to the sandbox. Just like a child that can only play in a sandbox, remote code was only allowed to paint on the screen and interact with the user. The applet security manager denied all access to local resources. JDK 1.1 implemented a slight modification: Remote code that was signed by a trusted entity was granted the same permissions as local classes. However, both versions of the JDK provided an all-or-nothing approach. Programs either had full access or they had to play in the sandbox. The Java 2 platform has a much more flexible mechanism. A security policy maps code sources to permission sets (see Figure 9-4). Figure 9-4. A security policyA code source has two properties: the code location (for example, an HTTP URL for remote code, or a file URL for local code in a JAR file) and certificates. You see later in this chapter how code can be certified by trusted parties. A permission is any property that is checked by a security manager. JDK 1.2 implementation supports a number of permission classes, each of which encapsulates the details of a particular permission. For example, the following instance of the FilePermission class states that it is okay to read and write any file in the /tmp directory. FilePermission p = new FilePermission("/tmp/*", "read,write"); More important, the default implementation of the Policy class in JDK 1.2 reads permissions from a permission file. Inside a permission file, the same read permission is expressed as permission java.io.FilePermission "/tmp/*", "read,write"; We discuss permission files in the next section. Figure 9-5 shows the hierarchy of the permission classes that were supplied with JDK 1.2. Many more permission classes have been added in subsequent JDK versions. Figure 9-5. The hierarchy of permission classesIn the preceding section, you saw that the SecurityManager class has security check methods such as checkExit. These methods exist only for the convenience of the programmer and for backward compatibility. They all map into standard permission checks. For example, here is the source code for the checkExit method: public void checkExit() { checkPermission(new RuntimePermission("exitVM")); } Each security manager is free to provide its own implementation of the checkPermission method. However, the JDK provides a "standard model" of how to carry out permission checks. For the remainder of this section, we describe this standard model. Each class has a protection domain, an object that encapsulates both the code source and the collection of permissions of the class. When the SecurityManager needs to check a permission, it looks at the classes of all methods currently on the call stack. It then gets the protection domains of all classes and asks each protection domain if its permission collection allows the operation that is currently being checked. If all domains agree, then the check passes. Otherwise, a SecurityException is thrown. Why do all methods on the call stack need to allow a particular operation? Let us work through an example. Suppose the init method of an applet wants to open a file. It might call Reader in = new FileReader(name); The FileReader constructor calls the FileInputStream constructor, which calls the checkRead method of the security manager, which finally calls checkPermission with a FilePermission(name, "read" object. Table 9-1 shows the call stack.
The FileInputStream and SecurityManager classes are system classes whose CodeSource is null and whose permissions consist of an instance of the AllPermission class, which allows all operations. Clearly, their permissions alone can't determine the outcome of the check. As you can see, the checkPermission method must take into account the restricted permissions of the applet class. By checking the entire call stack, the security mechanism ensures that one class can never ask another class to carry out a sensitive operation on its behalf. NOTE
java.lang.SecurityManager 1.0
java.lang.Class 1.0
java.security.ProtectionDomain 1.2
java.security.CodeSource 1.2
Security Policy FilesIn the preceding section, you saw how the SecureClassLoader assigns permissions when loading classes, that is, by asking a Policy object to look up the permissions for the code source of each class. In principle, you can install your own Policy class to carry out the mapping from code sources to permissions. However, in this section, you learn about the standard policy class that the Java 2 virtual machine uses. NOTE
The standard policy reads policy files that contain instructions for mapping code sources to permissions. You have seen these policy files in Chapter 5, where you saw that they were required to grant network access to programs that use the RMISecurityManager. Here is a typical policy file: grant codeBase "http://www.horstmann.com/classes" { permission java.io.FilePermission "/tmp/*", "read,write"; }; This file grants permission to read and write files in the /tmp directory to all code that was downloaded from http://www.horstmann.com/classes. You can install policy files in standard locations. By default, there are two locations:
NOTE
During testing, we don't like to constantly modify these standard files. Therefore, we prefer to explicitly name the policy file that is required for each application. Simply place the permissions into a separate file, say, MyApp.policy, and start the virtual machine as java -Djava.security.policy=MyApp.policy MyApp For applets, you instead use appletviewer -J-Djava.security.policy=MyApplet.policy MyApplet.html (You can use the -J option of the appletviewer to pass any command-line argument to the virtual machine.) In these examples, the MyApp.policy file is added to the other policies in effect. If you add a second equal sign, such as java -Djava.security.policy==MyApp.policy MyApp then your application uses only the specified policy file, and the standard policy files are ignored. CAUTION
As you saw previously, Java applications by default do not install a security manager. Therefore, you won't see the effect of policy files until you install one. You can, of course, add a line System.setSecurityManager(new SecurityManager()); into your main method. Or you can add the command-line option -Djava.security.manager when starting the virtual machine. java -Djava.security.manager -Djava.security.policy=MyApp.policy MyApp In the remainder of this section, we show you in detail how to describe permissions in the policy file. We describe the entire policy file format, except for code certificates, which we cover later in this chapter. A policy file contains a sequence of grant entries. Each entry has the following form:
The code source contains a code base (which can be omitted if the entry applies to code from all sources) and the names of trusted principals and certificate signers (which can be omitted if signatures are not required for this entry). The code base is specified as codeBase "url" If the URL ends in a /, then it refers to a directory. Otherwise, it is taken to be the name of a JAR file. For example, grant codeBase "www.horstmann.com/classes/" { . . . }; grant codeBase "www.horstmann.com/classes/MyApp.jar" { . . . }; The code base is a URL and should always contain forward slashes as file separators, even for file URLs in Windows. For example, grant codeBase "file:C:/myapps/classes/" { . . . }; NOTE
The permissions have the following structure:
The class name is the fully qualified class name of the permission class (such as java.io.FilePermission). The target name is a permission-specific value, for example, a file or directory name for the file permission, or a host and port for a socket permission. The actionList is also permission specific. It is a list of actions, such as read or connect, separated by commas. Some permission classes don't need target names and action lists. Table 9-2 lists the commonly used permission classes and their actions.
As you can see from Table 9-2, most permissions simply permit a particular operation. You can think of the operation as the target with an implied action "permit". These permission classes all extend the BasicPermission class (see Figure 9-5 on page 708). However, the targets for the file, socket, and property permissions are more complex, and we need to investigate them in detail. File permission targets can have the following form:
For example, the following permission entry gives access to all files in the directory /myapp and any of its subdirectories. permission java.io.FilePermission "/myapp/-", "read,write,delete"; You must use the \\ escape sequence to denote a backslash in a Windows file name. permission java.io.FilePermission "c:\\myapp\\-", "read,write,delete"; Socket permission targets consist of a host and a port range. Host specifications have the following form:
Port ranges are optional and have the form:
Here is an example: permission java.net.SocketPermission "*.horstmann.com:8000-8999", "connect"; Finally, property permission targets can have one of two forms:
Examples are "java.home" and "java.vm.*" For example, the following permission entry allows a program to read all properties that start with java.vm. permission java.util.PropertyPermission "java.vm.*", "read"; You can use system properties in policy files. The token ${property} is replaced by the property value. For example, ${user.home} is replaced by the home directory of the user. Here is a typical use of this system property in a permission entry. permission java.io.FilePermission "${user.home}", "read,write"; To create platform-independent policy files, it is a good idea to use the file.separator property instead of explicit / or \\ separators. To make this simpler, the special notation ${/} is a shortcut for ${file.separator}. For example, permission java.io.FilePermission "${user.home}${/}-", "read,write"; is a portable entry for granting permission to read and write in the user's home directory and any of its subdirectories. NOTE
Custom PermissionsIn this section, you see how you can supply your own permission class that users can refer to in their policy files. To implement your permission class, you extend the Permission class and supply the following methods:
The last method is the most important. Permissions have an ordering, in which more general permissions imply more specific ones. Consider the file permission p1 = new FilePermission("/tmp/-", "read, write"); This permission allows reading and writing of any file in the /tmp directory and any of its subdirectories. This permission implies other, more specific permissions: p2 = new FilePermission("/tmp/-", "read"); p3 = new FilePermission("/tmp/aFile", "read, write"); p4 = new FilePermission("/tmp/aDirectory/-", "write"); In other words, a file permission p1 implies another file permission p2 if
Consider the following example of the use of the implies method. When the FileInputStream constructor wants to open a file for reading, it checks whether it has permission to do so. For that check, a specific file permission object is passed to the checkPermission method: checkPermission(new FilePermission(fileName, "read")); The security manager now asks all applicable permissions whether they imply this permission. If any one of them implies it, then the check passes. In particular, the AllPermission implies all other permissions. If you define your own permission classes, then you need to define a suitable notion of implication for your permission objects. Suppose, for example, that you define a TVPermission for a set-top box powered by Java technology. A permission new TVPermission("Tommy:2-12:1900-2200", "watch,record") might allow Tommy to watch and record television channels 212 between 19:00 and 22:00. You need to implement the implies method so that this permission implies a more specific one, such as new TVPermission("Tommy:4:2000-2100", "watch") Implementation of a Permission ClassIn the next sample program, we implement a new permission for monitoring the insertion of text into a text area. The program ensures that you cannot add "bad words" such as sex, drugs, and C++ into a text area. We use a custom permission class so that the list of bad words can be supplied in a policy file. The following subclass of JTextArea asks the security manager whether it is okay to add new text. class WordCheckTextArea extends JTextArea { public void append(String text) { WordCheckPermission p = new WordCheckPermission(text, "insert"); SecurityManager manager = System.getSecurityManager(); if (manager != null) manager.checkPermission(p); super.append(text); } } If the security manager grants the WordCheckPermission, then the text is appended. Otherwise, the checkPermission method throws an exception. Word check permissions have two possible actions: insert (the permission to insert a specific text) and avoid (the permission to add any text that avoids certain bad words). You should run this program with the following policy file: grant { permission WordCheckPermission "sex,drugs,C++", "avoid"; }; This policy file grants the permission to insert any text that avoids the bad words sex, drugs, and C++. When designing the WordCheckPermission class, we must pay particular attention to the implies method. Here are the rules that control whether permission p1 implies permission p2.
You can find the implementation of this class in Example 9-5. Note that you retrieve the permission target with the confusingly named getName method of the Permission class. Since permissions are described by a pair of strings in policy files, permission classes need to be prepared to parse these strings. In particular, we use the following method to transform the comma-separated list of bad words of an avoid permission into a genuine Set. public Set<String> badWordSet() { Set<String> set = new HashSet<String>(); set.addAll(Arrays.asList(getName().split(","))); return set; } This code allows us to use the equals and containsAll methods to compare sets. As you saw in Chapter 2, the equals method of a set class finds two sets to be equal if they contain the same elements in any order. For example, the sets resulting from "sex,drugs,C++" and "C++,drugs,sex" are equal. CAUTION
The program in Example 9-4 shows how the WordCheckPermission class works. Type any text into the text field and click the Insert button. If the security check passes, the text is appended to the text area. If not, an error message is displayed (see Figure 9-7). Figure 9-7. The PermissionTest programMake sure to start the program with the appropriate policy file. java -Djava.security.policy=PermissionTest.policy PermissionTest Otherwise, all attempts to insert text will fail. CAUTION
Example 9-4. PermissionTest.java1. import java.awt.*; 2. import java.awt.event.*; 3. import java.io.*; 4. import java.net.*; 5. import java.security.*; 6. import java.util.*; 7. import javax.swing.*; 8. 9. /** 10. This class demonstrates the custom WordCheckPermission. 11. */ 12. public class PermissionTest 13. { 14. public static void main(String[] args) 15. { 16. System.setSecurityManager(new SecurityManager()); 17. JFrame frame = new PermissionTestFrame(); 18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 19. frame.setVisible(true); 20. } 21. } 22. 23. /** 24. This frame contains a text field for inserting words into 25. a text area that is protected from "bad words". 26. */ 27. class PermissionTestFrame extends JFrame 28. { 29. public PermissionTestFrame() 30. { 31. setTitle("PermissionTest"); 32. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 33. 34. textField = new JTextField(20); 35. JPanel panel = new JPanel(); 36. panel.add(textField); 37. JButton openButton = new JButton("Insert"); 38. panel.add(openButton); 39. openButton.addActionListener(new 40. ActionListener() 41. { 42. public void actionPerformed(ActionEvent event) 43. { 44. insertWords(textField.getText()); 45. } 46. }); 47. 48. add(panel, BorderLayout.NORTH); 49. 50. textArea = new WordCheckTextArea(); 51. add(new JScrollPane(textArea), BorderLayout.CENTER); 52. } 53. 54. /** 55. Tries to insert words into the text area. 56. Displays a dialog if the attempt fails. 57. @param words the words to insert 58. */ 59. public void insertWords(String words) 60. { 61. try 62. { 63. textArea.append(words + "\n"); 64. } 65. catch (SecurityException e) 66. { 67. JOptionPane.showMessageDialog(this, "I am sorry, but I cannot do that."); 68. } 69. } 70. 71. private JTextField textField; 72. private WordCheckTextArea textArea; 73. private static final int DEFAULT_WIDTH = 400; 74. private static final int DEFAULT_HEIGHT = 300; 75. } 76. 77. /** 78. A text area whose append method makes a security check 79. to see that no bad words are added. 80. */ 81. class WordCheckTextArea extends JTextArea 82. { 83. public void append(String text) 84. { 85. WordCheckPermission p = new WordCheckPermission(text, "insert"); 86. SecurityManager manager = System.getSecurityManager(); 87. if (manager != null) manager.checkPermission(p); 88. super.append(text); 89. } 90. } Example 9-5. WordCheckPermission.java1. import java.security.*; 2. import java.util.*; 3. 4. /** 5. A permission that checks for bad words. 6. */ 7. public class WordCheckPermission extends Permission 8. { 9. /** 10. Constructs a word check permission 11. @param target a comma separated word list 12. @param anAction "insert" or "avoid" 13. */ 14. public WordCheckPermission(String target, String anAction) 15. { 16. super(target); 17. action = anAction; 18. } 19. 20. public String getActions() { return action; } 21. 22. public boolean equals(Object other) 23. { 24. if (other == null) return false; 25. if (!getClass().equals(other.getClass())) return false; 26. WordCheckPermission b = (WordCheckPermission) other; 27. if (!action.equals(b.action)) return false; 28. if (action.equals("insert")) 29. return getName().equals(b.getName()); 30. else if (action.equals("avoid")) 31. return badWordSet().equals(b.badWordSet()); 32. else return false; 33. } 34. 35. public int hashCode() 36. { 37. return getName().hashCode() + action.hashCode(); 38. } 39. 40. public boolean implies(Permission other) 41. { 42. if (!(other instanceof WordCheckPermission)) return false; 43. WordCheckPermission b = (WordCheckPermission) other; 44. if (action.equals("insert")) 45. { 46. return b.action.equals("insert") && 47. getName().indexOf(b.getName()) >= 0; 48. } 49. else if (action.equals("avoid")) 50. { 51. if (b.action.equals("avoid")) 52. return b.badWordSet().containsAll(badWordSet()); 53. else if (b.action.equals("insert")) 54. { 55. for (String badWord : badWordSet()) 56. if (b.getName().indexOf(badWord) >= 0) 57. return false; 58. return true; 59. } 60. else return false; 61. } 62. else return false; 63. } 64. 65. /** 66. Gets the bad words that this permission rule describes. 67. @return a set of the bad words 68. */ 69. public Set<String> badWordSet() 70. { 71. Set<String> set = new HashSet<String>(); 72. set.addAll(Arrays.asList(getName().split(","))); 73. return set; 74. } 75. 76. private String action; 77. } java.security.Permission 1.2
A Custom Security ManagerIn this section, we show you how to build a simple security manager. In your own programs, you are better off using custom permissions rather than implementing a full security manager, but we thought you might find it interesting to see how a security manager works internally. Our security manager monitors all file access and ensures that you can't open a text file if it contains forbidden words such as sex, drugs, and C++. We monitor file access by overriding the checkPermission method of the standard security manager class. If the permission isn't a file read permission, then we simply call super.checkPermission. To check that it is permissible to read from a file, we open the file and scan its contents. We grant access to the file only when it doesn't contain any of the forbidden words. (We only monitor files with extension .txt since we don't want to block access to system and property files.) public class WordCheckSecurityManager extends SecurityManager { public void checkPermission(Permission p) { if (p instanceof FilePermission && p.getActions().equals("read")) { String fileName = p.getName(); if (containsBadWords(fileName)) throw new SecurityException("Bad words in " + fileName); } else super.checkPermission(p); } . . . } NOTE
There is just one catch in our file check scenario. Consider one possible flow of events.
But the containsBadWords method must itself read the file to check its contents, which calls the security manager again! This would result in an infinite regression unless the security manager has a way of finding out in which context it was called. The getClassContext method is the way to find out how the method was called. This method returns an array of class objects that gives all the classes whose calls are currently pending. For example, when the security manager is called for the first time, that array is class WordCheckSecurityManager class SecurityManager class java.io.FileInputStream class java.io.FileReader class SecurityManagerFrame . . . class java.awt.EventDispatchThread The class at index 0 gives the currently executing call. Unfortunately, you only get to see the classes, not the names of the pending methods. When the security manager itself attempts to open the file, it is called again and the getClassContext method returns the following array: class WordCheckSecurityManager class SecurityManager class java.io.FileInputStream class java.io.FileReader class WordCheckSecurityManager class WordCheckSecurityManager class SecurityManager class java.io.FileInputStream class java.io.FileReader class SecurityManagerFrame . . . class java.awt.EventDispatchThread In this case, the security manager should permit the file access. How can we do this? We could test whether getClassContext()[0] == getClassContext()[4] but this approach is fragile. Here's an obvious case of where it can go wrong: Imagine that if the implementation changed, for example, so the FileReader constructor calls the security manager directly, then the test would be meaningless because the positions would not be the same in the array. It is far more robust to test whether any of the pending calls came from the same security manager. Here is a method that carries out this test. Since this method is called from checkPermission, there are at least two copies of the security manager class on the call stack. We skip these first and then look for another instance of the same security manager. boolean inSameManager() { Class[] cc = getClassContext(); // skip past current set of calls to this manager int i = 0; while (i < cc.length && cc[0] == cc[i]) i++; // check if there is another call to this manager while (i < cc.length) { if (cc[0] == cc[i]) return true; i++; } return false; } We call this method in the checkPermission method. If we find that the security manager is invoked recursively, then we do not call the containsBadWords method again. if (p instanceof FilePermission && p.getActions().equals("read")) { if (inSameManager()) return; String fileName = p.getName(); if (containsBadWords(fileName)) throw new SecurityException("Bad words in " + fileName); } Example 9-6 shows a program that puts this security manager to work. The security manager is installed in the main method. When running the program, you can specify a file. The program will load its contents into the text area. However, if the file fails the security check, the program catches the security exception and displays a message instead (see Figure 9-8). For example, you can display "Alice in Wonderland," but the program refuses to load "The Count of Monte Cristo." Figure 9-8. The SecurityManagerTest programNOTE
Be careful how you invoke this program. The WordCheckSecurityManager class itself needs to be given AllPermission. The reason for this is subtle. The WordCheckSecurityManager class calls the SecurityManager superclass for all permissions other than file read permissions. When the SecurityManager class evaluates a permission, it checks whether all methods on the call stack should be granted that particular permission. The WordCheckSecurityManager is one of those classes. But it is not a system class, so you must explicitly grant it all permissions without also granting all permissions to the other classes of the program. To separate the WordCheckSecurityManager class files from the other class files, make a JAR file containing just that class file. jar cvf WordCheck.jar WordCheckSecurityManager.class Then delete the WordCheckSecurityManager.class file. Next, create a policy file, WordCheck.policy, with the following contents: grant codeBase "file:WordCheck.jar" { permission java.security.AllPermission; }; This policy grants all permissions to the classes in the WordCheck.jar file. Finally, start the application as follows: java -classpath .:WordCheck.jar -Djava.security.policy=WordCheck.policy SecurityManagerTest On Windows, use a semicolon to separate the class path elements: java -classpath .;WordCheck.jar . . . TIP
Example 9-6. SecurityManagerTest.java1. import java.awt.*; 2. import java.awt.event.*; 3. import java.io.*; 4. import java.net.*; 5. import java.util.*; 6. import javax.swing.*; 7. 8. /** 9. This class demonstrates the use of a custom security manager 10. that prohibits the reading of text files containing bad words. 11. */ 12. public class SecurityManagerTest 13. { 14. public static void main(String[] args) 15. { 16. System.setSecurityManager(new WordCheckSecurityManager()); 17. JFrame frame = new SecurityManagerFrame(); 18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 19. frame.setVisible(true); 20. } 21. } 22. 23. /** 24. This frame contains a text field to enter a file name and 25. a text area to show the contents of the loaded file. 26. */ 27. class SecurityManagerFrame extends JFrame 28. { 29. public SecurityManagerFrame() 30. { 31. setTitle("SecurityManagerTest"); 32. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 33. 34. fileNameField = new JTextField(20); 35. JPanel panel = new JPanel(); 36. panel.add(new JLabel("Text file:")); 37. panel.add(fileNameField); 38. JButton openButton = new JButton("Open"); 39. panel.add(openButton); 40. openButton.addActionListener(new 41. ActionListener() 42. { 43. public void actionPerformed(ActionEvent event) 44. { 45. loadFile(fileNameField.getText()); 46. } 47. }); 48. 49. add(panel, "North"); 50. 51. fileText = new JTextArea(); 52. add(new JScrollPane(fileText), "Center"); 53. } 54. 55. /** 56. Attempt to load a file into the text area. If a security exception is caught, 57. a message is inserted into the text area instead. 58. @param filename the file name 59. */ 60. public void loadFile(String filename) 61. { 62. try 63. { 64. fileText.setText(""); 65. Scanner in = new Scanner(new FileReader(filename)); 66. while (in.hasNextLine()) fileText.append(in.nextLine() + "\n"); 67. in.close(); 68. } 69. catch (IOException e) 70. { 71. fileText.append(e + "\n"); 72. } 73. catch (SecurityException e) 74. { 75. fileText.append("I am sorry, but I cannot do that.\n"); 76. fileText.append(e + "\n"); 77. } 78. } 79. 80. private JTextField fileNameField; 81. private JTextArea fileText; 82. private static final int DEFAULT_WIDTH = 400; 83. private static final int DEFAULT_HEIGHT = 300; 84. } Example 9-7. WordCheckSecurityManager.java1. import java.io.*; 2. import java.security.*; 3. import java.util.*; 4. 5. /** 6. This security manager checks whether bad words are 7. encountered when reading a file. 8. */ 9. public class WordCheckSecurityManager extends SecurityManager 10. { 11. public void checkPermission(Permission p) 12. { 13. if (p instanceof FilePermission && p.getActions().equals("read")) 14. { 15. if (inSameManager()) 16. return; 17. String fileName = p.getName(); 18. if (containsBadWords(fileName)) 19. throw new SecurityException("Bad words in " + fileName); 20. } 21. else super.checkPermission(p); 22. } 23. 24. /** 25. Returns true if this manager is called while there 26. is another call to itself pending. 27. @return true if there are multiple calls to this manager 28. */ 29. public boolean inSameManager() 30. { 31. Class[] cc = getClassContext(); 32. 33. // skip past current set of calls to this manager 34. int i = 0; 35. while (i < cc.length && cc[0] == cc[i]) 36. i++; 37. 38. // check if there is another call to this manager 39. while (i < cc.length) 40. { 41. if (cc[0] == cc[i]) return true; 42. i++; 43. } 44. return false; 45. } 46. 47. /** 48. Checks if a file contains bad words. 49. @param fileName the name of the file 50. @return true if the file name ends with .txt and it 51. contains at least one bad word. 52. */ 53. boolean containsBadWords(String fileName) 54. { 55. if (!fileName.toLowerCase().endsWith(".txt")) return false; 56. // only check text files 57. Scanner in; 58. try 59. { 60. in = new Scanner(new FileReader(fileName)); 61. } 62. catch (IOException e) 63. { 64. return false; 65. } 66. while (in.hasNext()) 67. if (badWords.contains(in.next().toLowerCase())) 68. { 69. in.close(); 70. System.out.println(fileName); 71. return true; 72. } 73. return false; 74. } 75. 76. private List badWords = Arrays.asList(new String[] { "sex", "drugs", "c++" }); 77. } java.lang.System 1.0
java.lang.SecurityManager 1.0
User AuthenticationStarting with JDK 1.4, the Java Authentication and Authorization Service (JAAS) is included in the Java 2 Standard Edition. The "authentication" part is concerned with ascertaining the identity of a program user. The "authorization" part maps users to permissions. JAAS is a "pluggable" API that isolates Java applications from the particular technology used to implement authentication. It supports, among others, UNIX logins, NT logins, Kerberos authentication, and certificate-based authentication. Once a user has been authenticated, you can attach a set of permissions. For example, here we grant Harry a particular set of permissions that other users do not have. The syntax is like this: grant principal com.sun.security.auth.UnixPrincipal "harry" { permission java.util.PropertyPermission "user.*", "read"; . . . }; The com.sun.security.auth.UnixPrincipal class checks the name of the UNIX user who is running this program. Its getName method returns the UNIX login name, and we check whether that name equals "harry". You use a LoginContext to allow the security manager to check such a grant statement. Here is the basic outline of the login code: try { System.setSecurityManager(new SecurityManager()); LoginContext context = new LoginContext("Login1"); // defined in JAAS configuration file context.login(); // get the authenticated Subject Subject subject = context.getSubject(); . . . context.logout(); } catch (LoginException exception) // thrown if login was not successful { exception.printStackTrace(); } Now the subject denotes the individual who has been authenticated. The string parameter "Login1" in the LoginContext constructor refers to an entry with the same name in the JAAS configuration file. Here is a sample configuration file: Login1 { com.sun.security.auth.module.UnixLoginModule required; com.whizzbang.auth.module.RetinaScanModule sufficient; }; Login2 { . . . }; Of course, the JDK contains no biometric login modules. The JDK contains the following modules in the com.sun.security.auth.module package: UnixLoginModule NTLoginModule Krb5LoginModule JndiLoginModule KeyStoreLoginModule A login policy consists of a sequence of login modules, each of which is labeled required, sufficient, requisite, or optional. The modules are executed in turn, until a sufficient module succeeds, a requisite module fails, or the end of the module list is reached. Authentication is successful if all required and requisite modules succeed, or if none of them were executed, if at least one sufficient or optional module succeeds. A login authenticates a subject, which can have multiple principals. A principal describes some property of the subject, such as the user name, group ID, or role. As you saw in the grant statement, principals govern permissions. The com.sun.security.auth.UnixPrincipal describes the UNIX login name, and the UnixNumericGroupPrincipal can test for membership in a UNIX group. A grant clause can test for a principal, with the syntax
For example: grant com.sun.security.auth.UnixPrincipal "harry" When a user has logged in, you then run, in a separate access control context, the code that requires checking of principals. Use the static doAs or doAsPrivileged method to start a new PrivilegedAction whose run method executes the code. Both of those methods execute an action by calling the run method of an object that implements the PrivilegedAction interface, using the permissions of the subject's principals: PrivilegedAction action = new PrivilegedAction() { public Object run() { // run with permissions of subject principals . . . } }; Subject.doAsPrivileged(subject, action, null); // or doAs(subject, action) If the actions can throw checked exceptions, then you implement the PrivilegedExceptionAction interface instead. The difference between the doAs and doAsPrivileged methods is subtle. The doAs method starts out with the current access control context, whereas the doAsPrivileged method starts out with a new context. The latter method allows you to separate the permissions for the login code and the "business logic." In our example application, the login code has permissions permission javax.security.auth.AuthPermission "createLoginContext.Login1"; permission javax.security.auth.AuthPermission "doAsPrivileged"; The authenticated user has a permission permission java.util.PropertyPermission "user.*", "read"; If we had used doAs instead of doAsPrivileged, then the login code would have also needed that permission! The program in Example 9-8 and Example 9-9 demonstrates how to restrict permissions to certain users. The AuthTest program authenticates a user and then runs a simple action that retrieves a system property. To make this example work, package the code for the login and the action into two separate JAR files: javac AuthTest.java jar cvf login.jar AuthTest*.class javac SysPropAction.java jar cvf action.jar SysPropAction.class If you look at the policy file in Example 9-10, you will see that the UNIX user with the name harry has the permission to read all files. Change harry to your login name. Then run the command java -classpath login.jar:action.jar -Djava.security.policy=AuthTest.policy -Djava.security.auth.login.config=jaas.config AuthTest Example 9-11 shows the login configuration. On Windows, change Unix to NT in both AuthTest.policy and jaas.config, and use a semicolon to separate the JAR files: java -classpath login.jar;action.jar . . . The AuthTest program should now display the value of the user.home property. However, if you change the login name in the AuthTest.policy file, then a security exception should be thrown because you no longer have the required permission. CAUTION
Example 9-8. AuthTest.java[View full width] 1. import java.security.*; 2. import javax.security.auth.*; 3. import javax.security.auth.login.*; 4. 5. /** 6. This program authenticates a user via a custom login and then executes the SysPropAction 7. with the user's privileges. 8. */ 9. public class AuthTest 10. { 11. public static void main(final String[] args) 12. { 13. try 14. { 15. System.setSecurityManager(new SecurityManager()); 16. LoginContext context = new LoginContext("Login1"); 17. context.login(); 18. System.out.println("Authentication successful."); 19. Subject subject = context.getSubject(); 20. System.out.println("subject=" + subject); 21. PrivilegedAction action = new SysPropAction("user.home"); 22. Object result = Subject.doAsPrivileged(subject, action, null); 23. System.out.println(result); 24. context.logout(); 25. } 26. catch (LoginException e) 27. { 28. e.printStackTrace(); 29. } 30. } 31. } Example 9-9. SysPropAction.java1. import java.security.*; 2. 3. /** 4. This action looks up a system property. 5. */ 6. public class SysPropAction implements PrivilegedAction 7. { 8. /** 9. Constructs an action for looking up a given property. 10. @param propertyName the property name (such as "user.home") 11. */ 12. public SysPropAction(String propertyName) { this.propertyName = propertyName; } 13. 14. public Object run() 15. { 16. return System.getProperty(propertyName); 17. } 18. 19. private String propertyName; 20. } Example 9-10. AuthTest.policy1. grant codebase "file:login.jar" 2. { 3. permission javax.security.auth.AuthPermission "createLoginContext.Login1"; 4. permission javax.security.auth.AuthPermission "doAsPrivileged"; 5. }; 6. 7. grant principal com.sun.security.auth.UnixPrincipal "harry" 8. { 9. permission java.util.PropertyPermission "user.*", "read"; 10. }; javax.security.auth.login.LoginContext 1.4
javax.security.auth.Subject 1.4
java.security.PrivilegedAction 1.4
java.security.PrivilegedExceptionAction 1.4
java.security.Principal 1.1
JAAS Login ModulesIn this section, we look at a JAAS example that shows you
Supplying your own login module is useful if you store login information in a database. Even if you are happy with the default module, studying a custom module will help you understand the JAAS configuration file options. Role-based authentication is essential if you manage a large number of users. It would be impractical to put the names of all legitimate users into a policy file. Instead, the login module should map users to roles such as "admin" or "HR," and the permissions should be based on these roles. One job of the login module is to populate the principal set of the subject that is being authenticated. If a login module supports roles, it adds Principal objects that describe roles. The JDK does not provide a class for this purpose, so we wrote our own (see Example 9-12). The class simply stores a description/value pair, such as role=admin. Its getName method returns that pair, so we can add role-based permissions into a policy file: grant principal SimplePrincipal "role=admin" { . . . } Our login module (see Example 9-13) looks up users, passwords, and roles in a text file that contains lines like this: harry|wombat|admin carl|mockturtle|HR Of course, in a realistic login module, you would store this information in a database or directory. You can find the code for the SimpleLoginModule in Example 9-13. The checkLogin method checks whether the user name and password match a user record in the password file. If so, we add two SimplePrincipal objects to the subject's principal set: Set<Principal> principals = subject.getPrincipals(); principals.add(new SimplePrincipal("username", username)); principals.add(new SimplePrincipal("role", role)); The remainder of SimpleLoginModule is straightforward plumbing. The initialize method receives
For example, we configure our module as follows: SimpleLoginModule required pwfile="password.txt"; The login module retrieves the pwfile settings from the options map. The login module does not gather the user name and password; that is the job of a separate handler. This separation allows you to use the same login module without worrying whether the login information comes from a GUI dialog, a console prompt, or a configuration file. The handler is specified when you construct the LoginContext, for example, LoginContext context = new LoginContext("Login1", new com.sun.security.auth.callback.DialogCallbackHandler()); The DialogCallbackHandler pops up a simple GUI dialog box to retrieve the user name and password. com.sun.security.auth.callback.TextCallbackHandler gets the information from the console. However, in our application, we have our own GUI for collecting the user name and password (see Figure 9-9). We produce a simple handler that merely stores and returns that information (see Example 9-14). Figure 9-9. A custom login moduleThe handler has a single method, handle, that processes an array of Callback objects. A number of predefined classes, such as NameCallback and PasswordCallback, implement the Callback interface. You could also add your own class, such as RetinaScanCallback. The handler code is a bit unsightly because it needs to analyze the types of the callback objects: public void handle(Callback[] callbacks) { for (Callback callback : callbacks) { if (callback instanceof NameCallback) . . . else if (callback instanceof PasswordCallback) . . . else . . . } } The login module prepares an array of the callbacks that it needs for authentication: NameCallback nameCall = new NameCallback("username: "); PasswordCallback passCall = new PasswordCallback("password: ", false); callbackHandler.handle(new Callback[] { nameCall, passCall }); Then it retrieves the information from the callbacks. The program in Example 9-11 displays a form for entering the login information and the name of a system property. If the user is authenticated, the property value is retrieved in a PrivilegedAction. As you can see from the policy file in Example 9-15, only users with the admin role have permission to read properties. As in the preceding section, you must separate the login and action code. Create two JAR files: javac *.java jar cvf login.jar JAAS*.class Simple*.class jar cvf action.jar SysPropAction.class Then run the program as java -classpath login.jar:action.jar -Djava.security.policy=JAASTest.policy -Djava.security.auth.login.config=jaas.config JAASTest Example 9-16 shows the login configuration. It is possible to support a more complex two-phase protocol, whereby a login is committed if all modules in the login configuration were successful. For more information, see the login module developer's guide at http://java.sun.com/j2se/5.0/docs/guide/security/jaas/JAASLMDevGuide.html. Example 9-11. JAASTest.java[View full width] 1. import java.awt.*; 2. import java.awt.event.*; 3. import javax.security.auth.*; 4. import javax.security.auth.login.*; 5. import javax.swing.*; 6. 7. /** 8. This program authenticates a user via a custom login and then executes the SysPropAction 9. with the user's privileges. 10. */ 11. public class JAASTest 12. { 13. public static void main(final String[] args) 14. { 15. System.setSecurityManager(new SecurityManager()); 16. JFrame frame = new JAASFrame(); 17. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 18. frame.setVisible(true); 19. } 20. } 21. 22. /** 23. This frame has text fields for user name and password, a field for the name of the requested 24. system property, and a field to show the property value. 25. */ 26. class JAASFrame extends JFrame 27. { 28. public JAASFrame() 29. { 30. setTitle("JAASTest"); 31. 32. username = new JTextField(20); 33. password = new JPasswordField(20); 34. propertyName = new JTextField(20); 35. propertyValue = new JTextField(20); 36. propertyValue.setEditable(false); 37. 38. JPanel panel = new JPanel(); 39. panel.setLayout(new GridLayout(0, 2)); 40. panel.add(new JLabel("username:")); 41. panel.add(username); 42. panel.add(new JLabel("password:")); 43. panel.add(password); 44. panel.add(propertyName); 45. panel.add(propertyValue); 46. add(panel, BorderLayout.CENTER); 47. 48. JButton getValueButton = new JButton("Get Value"); 49. getValueButton.addActionListener(new 50. ActionListener() 51. { 52. public void actionPerformed(ActionEvent event) { getValue(); } 53. }); 54. JPanel buttonPanel = new JPanel(); 55. buttonPanel.add(getValueButton); 56. add(buttonPanel, BorderLayout.SOUTH); 57. pack(); 58. } 59. 60. public void getValue() 61. { 62. try 63. { 64. LoginContext context = new LoginContext("Login1", 65. new SimpleCallbackHandler(username.getText(), password.getPassword())); 66. context.login(); 67. Subject subject = context.getSubject(); 68. propertyValue.setText( 69. "" + Subject.doAsPrivileged(subject, new SysPropAction(propertyName .getText()), null)); 70. context.logout(); 71. } 72. catch (LoginException e ) 73. { 74. JOptionPane.showMessageDialog(this, e); 75. } 76. } 77. 78. private JTextField username; 79. private JPasswordField password; 80. private JTextField propertyName; 81. private JTextField propertyValue; 82. } Example 9-12. SimplePrincipal.java[View full width] 1. import java.security.*; 2. 3. /** 4. A principal with a named value (such as "role=HR" or "username=harry"). 5. */ 6. public class SimplePrincipal implements Principal 7. { 8. /** 9. Constructs a SimplePrincipal to hold a description and a value. 10. @param roleName the role name 11. */ 12. public SimplePrincipal(String descr, String value) { this.descr = descr; this.value = value; } 13. 14. /** 15. Returns the role name of this principal 16. @return the role name 17. */ 18. public String getName() { return descr + "=" + value; } 19. 20. public boolean equals(Object otherObject) 21. { 22. if (this == otherObject) return true; 23. if (otherObject == null) return false; 24. if (getClass() != otherObject.getClass()) return false; 25. SimplePrincipal other = (SimplePrincipal) otherObject; 26. return getName().equals(other.getName()); 27. } 28. public int hashCode() { return getName().hashCode(); } 29. 30. private String descr; 31. private String value; 32. } Example 9-13. SimpleLoginModule.java[View full width] 1. import java.io.*; 2. import java.lang.reflect.*; 3. import java.security.*; 4. import java.util.*; 5. import javax.security.auth.*; 6. import javax.security.auth.login.*; 7. import javax.security.auth.callback.*; 8. import javax.security.auth.spi.*; 9. 10. import javax.swing.*; 11. 12. /** 13. This login module authenticates users by reading usernames, passwords, and roles 14. from a text file. 15. */ 16. public class SimpleLoginModule implements LoginModule 17. { 18. public void initialize(Subject subject, CallbackHandler callbackHandler, 19. Map<String, ?> sharedState, Map<String, ?> options) 20. { 21. this.subject = subject; 22. this.callbackHandler = callbackHandler; 23. this.sharedState = sharedState; 24. this.options = options; 25. } 26. 27. public boolean login() throws LoginException 28. { 29. if (callbackHandler == null) 30. throw new LoginException("no handler"); 31. 32. NameCallback nameCall = new NameCallback("username: "); 33. PasswordCallback passCall = new PasswordCallback("password: ", false); 34. try 35. { 36. callbackHandler.handle(new Callback[] { nameCall, passCall }); 37. } 38. catch (UnsupportedCallbackException e) 39. { 40. LoginException e2 = new LoginException("Unsupported callback"); 41. e2.initCause(e); 42. throw e2; 43. } 44. catch (IOException e) 45. { 46. LoginException e2 = new LoginException("I/O exception in callback"); 47. e2.initCause(e); 48. throw e2; 49. } 50. 51. return checkLogin(nameCall.getName(), passCall.getPassword()); 52. } 53. 54. /** 55. Checks whether the authentication information is valid. If it is, the subject acquires 56. principals for the user name and role. 57. @param username the user name 58. @param password a character array containing the password 59. @return true if the authentication information is valid 60. */ 61. private boolean checkLogin(String username, char[] password) throws LoginException 62. { 63. try 64. { 65. Scanner in = new Scanner(new FileReader("" + options.get("pwfile"))); 66. while (in.hasNextLine()) 67. { 68. String[] inputs = in.nextLine().split("\\|"); 69. if (inputs[0].equals(username) && Arrays.equals(inputs[1].toCharArray(), password)) 70. { 71. String role = inputs[2]; 72. Set<Principal> principals = subject.getPrincipals(); 73. principals.add(new SimplePrincipal("username", username)); 74. principals.add(new SimplePrincipal("role", role)); 75. return true; 76. } 77. } 78. in.close(); 79. return false; 80. } 81. catch (IOException e) 82. { 83. LoginException e2 = new LoginException("Can't open password file"); 84. e2.initCause(e); 85. throw e2; 86. } 87. } 88. 89. public boolean logout() { return true; } 90. public boolean abort() { return true; } 91. public boolean commit() { return true; } 92. 93. private Subject subject; 94. private CallbackHandler callbackHandler; 95. private Map<String, ?> sharedState; 96. private Map<String, ?> options; 97. } Example 9-14. SimpleCallbackHandler.java1. import javax.security.auth.callback.*; 2. 3. /** 4. This simple callback handler presents the given user name and password. 5. */ 6. public class SimpleCallbackHandler implements CallbackHandler 7. { 8. /** 9. Constructs the callback handler. 10. @param username the user name 11. @param password a character array containing the password 12. */ 13. public SimpleCallbackHandler(String username, char[] password) 14. { 15. this.username = username; 16. this.password = password; 17. } 18. 19. public void handle(Callback[] callbacks) 20. { 21. for (Callback callback : callbacks) 22. { 23. if (callback instanceof NameCallback) 24. { 25. ((NameCallback) callback).setName(username); 26. } 27. else if (callback instanceof PasswordCallback) 28. { 29. ((PasswordCallback) callback).setPassword(password); 30. } 31. } 32. } 33. 34. private String username; 35. private char[] password; 36. } Example 9-15. JAASTest.policy1. grant codebase "file:login.jar" 2. { 3. permission java.awt.AWTPermission "showWindowWithoutWarningBanner"; 4. permission javax.security.auth.AuthPermission "createLoginContext.Login1"; 5. permission javax.security.auth.AuthPermission "doAsPrivileged"; 6. permission javax.security.auth.AuthPermission "modifyPrincipals"; 7. permission java.io.FilePermission "password.txt", "read"; 8. }; 9. 10. grant principal SimplePrincipal "role=admin" 11. { 12. permission java.util.PropertyPermission "*", "read"; 13. }; Example 9-16. jaas.config1. Login1 2. { 3. SimpleLoginModule required pwfile="password.txt"; 4. }; javax.security.auth.callback.CallbackHandler 1.4
javax.security.auth.callback.NameCallback 1.4
javax.security.auth.callback.PasswordCallback 1.4
javax.security.auth.spi.LoginModule 1.4
|
|