Monday Mar 15, 2010

Subversion authorization through LDAP with OpenDS

If you building a centralized development environment for a team or large group of users, the question of centralizing user identities, authentication and authorization is always popping up and the answer is often to use an LDAP directory server. The developer section of the OpenDS documentation wiki has a set of tutorials for using the OpenDS LDAP directory server with various web servers and open source project like GlassFish, Apache Tomcat, SugarCRM... But not yet for Subversion. Thankfully, Wooter van Reeven, Senior Consultant at Yenlo has just published a long and detailed tutorial for setting up Subversion authentication and authorization through LDAP, with OpenDS and Apache2.

Update on March 18th.

Wooter has also posted a copy of the article on OpenDS documentation wiki.

I've also been aware of an older article on the subject of Subversion with Apache and LDAP by Jeremy Whitlock, engineer in the CollabNet Subversion team. This article contains more details on the Apache configuration parameters and snippets for both Apache 2.0 and Apache 2.2.

Technorati Tags: , , , , , ,

Monday Jul 20, 2009

LDAP Tip : Counting the number of entries in a branch...

This is a general LDAP tip and it applies to OpenDS the open source LDAP directory service in Java, as well as Sun Directory Server (all versions) and other LDAP servers:
How can I know the number of entries under a specific node of the Directory Information Tree ?
Well it's simple. Every entry contains an operational attribute that specify the number of immediate subordinates entry : numSubordinates.
So to retrieve the number of entries under a specific node of the DIT, for example in the ou=people,dc=example,dc=com, a simple read is required.

$ bin/ldapsearch -p 3389 -D "cn=directory manager" -w - -b "ou=people,dc=example,dc=com" -s base '(objectclass=\*)' numsubordinates

Password for user 'cn=directory manager':
dn: ou=People,dc=example,dc=com
numsubordinates: 21

This attribute is defined in an expired Internet-Draft but has been well implemented in many servers. There are often some limitations, like the value only counts entries on the same server, but overall it's a very useful attribute especially when browsing through the DIT.

OpenDS and Sun Directory Server also implements another attribute : hasSubordinates, defined in X.501. hasSubordinates is a boolean and returns "true" or "false" depending on whether the entry is a branch or a leaf in the Directory Information Tree.

Technorati Tags: , , , ,

Thursday Mar 12, 2009

OpenDS Tips: Adding schema from OpenLDAP

Opends Logo TagThe OpenDS schema is slightly different from the OpenLDAP one, but it's quite simple to convert schema files from one format to another.
OpenDS, like Sun Directory Server Enterprise Edition and Fedora DS, uses a strict RFC 4512 and LDIF format.
In OpenLDAP, the actual text of the schema definition is similar and described using the RFC 4512 notation but uses the printer friendly notation, similar to the textual description in RFC documents.

So when converting schema files from OpenLDAP, for use in OpenDS, there are mainly 4 differences to take care of:

  • In OpenLDAP, an attribute definition begins with "attributetype" while in OpenDS it begins with "attributetypes: "
  • Similarly, in OpenLDAP, an object class definition has an "objectclass" prefix while it is "objectclasses: "
  • OpenDS follows the LDIF conventions that the continuation line begins with a single space character, and that an empty line is an entry separator
  • Finally, OpenDS schema files have a .ldif extension and only this extension is considered when loading schema from the config/schema directory.

The following python script can be used to convert an OpenLDAP schema file to a format usable by OpenDS (as well as Sun Directory Enterprise Edition). The script also recursively expands the OID macro format used in OpenLDAP schema files.
For now, syntax definitions are currently ignored as they cannot be loaded in OpenDS as they require associated code.

Usage is quite simple: schema-convert.py -o result.ldif openldap-schema-file


Enjoy and don't hesitate to send feedback, suggestions for improvements.

Update on March 15: I've added support for name prefixed OIDs substitution as suggested by Martin Gwerder.

Update on April 9: OpenDS schema files uses the .ldif extension, and only files with this extension are loaded by the server from the config/schema directory.

Update on July 31: Now checking and removing quotes around Sup or Syntaxes values.


#!/usr/bin/env python
# encoding: utf-8
"""
schema-convert.py

# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License, Version 1.0 only
# (the "License").  You may not use this file except in compliance
# with the License.
#
# You can obtain a copy of the license at
# trunk/opends/resource/legal-notices/OpenDS.LICENSE
# or https://OpenDS.dev.java.net/OpenDS.LICENSE.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at
# trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
# add the following below this CDDL HEADER, with the fields enclosed
# by brackets "[]" replaced with your own identifying information:
#      Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
#      Copyright 2009 Sun Microsystems, Inc.

Created by Ludovic Poitou on 2009-01-28.

This program converts an OpenLDAP schema file to the OpenDS schema file format.
"""

import sys
import getopt
import re
import string

help_message = '''
Usage: schema-convert.py [options] <openldap-schema-file>
options:
\\t -o output : specifies the output file, otherwise stdout is used
\\t -v : verbose mode
'''


class Usage(Exception):
	def __init__(self, msg):
		self.msg = msg


def main(argv=None):
	output = ""
	seclineoid = 0
	IDs = {}
	if argv is None:
		argv = sys.argv
	try:
		try:
			opts, args = getopt.getopt(argv[1:], "ho:v", ["help", "output="])
		except getopt.error, msg:
			raise Usage(msg)
	
		# option processing
		for option, value in opts:
			if option == "-v":
				verbose = True
			if option in ("-h", "--help"):
				raise Usage(help_message)
			if option in ("-o", "--output"):
				output = value
		
		
	except Usage, err:
		print >> sys.stderr, sys.argv[0].split("/")[-1] + ": " + str(err.msg)
		print >> sys.stderr, "\\t for help use --help"
		return 2
	try:
		infile = open(args[0], "r")
	except Usage, err:
		print >> sys.stderr, "Can't open file: " + str(err.msg)
	if output != "":
		try:
			outfile = open(output, "w")
		except Usage, err:
			print >> sys.stderr, "Can't open output file: " + str(err.msg)
	else:
		outfile = sys.stdout
	outfile.write("dn: cn=schema\\n")
	outfile.write("objectclass: top\\n")
	outfile.write("")
	for i in infile:
		newline = ""
		if not i.strip():
			continue
		#if i.startswith("#"):
		#	continue
		if re.match("objectidentifier", i, re.IGNORECASE):
			# Need to fill in an array of identifiers
			oid = i.split()
			if not re.match ("[0-9.]+", oid[2]):
				suboid = oid[2].split(':')
				IDs[oid[1]] = IDs[suboid[0]] + "." + suboid[1]
			else:	
				IDs[oid[1]] = oid[2]
			continue
		if seclineoid == 1:
			subattr = i.split()			
			if not re.match("[0-9.]+", subattr[0]):
				if re.match (".\*:", subattr[0]):
					# The OID is an name prefixed OID. Replace string with the OID
					suboid = subattr[0].split(":")
					repl = IDs[suboid[0]] + "." + suboid[1]
				else:
					# The OID is a name. Replace string with the OID
					repl = IDs[subattr[0]]
				newline = string.replace(i, subattr[0], repl, 1)
			seclineoid = 0
			
		if re.match("attributetype ", i, re.IGNORECASE):
			newline = re.sub("attribute[tT]ype", "attributeTypes:", i)
			# replace OID string with real OID if necessary
			subattr = newline.split()
			if len(subattr) < 3:
				seclineoid = 1
			else: 
				if not re.match("[0-9.]+", subattr[2]):
					if re.match (".\*:", subattr[2]):
						# The OID is an name prefixed OID. Replace string with the OID
						suboid = subattr[2].split(":")
						repl = IDs[suboid[0]] + "." + suboid[1]
					else:
						# The OID is a name. Replace string with the OID
						repl = IDs[subattr[2]]
					newline = string.replace(newline, subattr[2], repl, 1)
				
		if re.match("objectclass ", i, re.IGNORECASE):
			newline = re.sub("object[cC]lass", "objectClasses:", i)
			# replace OID String with real OID
			subattr = newline.split()
			if len(subattr) < 3:
				seclineoid = 1	
			else:
				if not re.match("[0-9.]+", subattr[2]):
					if re.match (".\*:", subattr[2]):
						# The OID is an name prefixed OID. Replace string with the OID
						suboid = subattr[2].split(":")
						repl = IDs[suboid[0]] + "." + suboid[1]
					else:
						# The OID is a name. Replace string with the OID
						repl = IDs[subattr[2]]
					newline = string.replace(newline, subattr[2], repl, 1)

		# Remove quoted syntax.
		if re.search("SYNTAX\\s'[\\d.]+'", newline):
			# Found a quoted syntax in an already updated line
			newline = re.sub("SYNTAX '([\\d.]+)'", "SYNTAX \\g<1>", newline)
		else:
			if re.search("SYNTAX\\s'[\\d.]+'", i):
				# Found a quoted syntax in the original line
				newline = re.sub("SYNTAX '([\\d.]+)'", "SYNTAX \\g<1>", i)

		# Remove quoted SUP
		if re.search("SUP\\s'[\\w\\-]+'", newline):
			# Found a quoted sup in an already updated line
			newline = re.sub("SUP '([\\w\\-]+)'", "SUP \\g<1>", newline)
		else:
			if re.search("SUP\\s'[\\w\\-]+'", i):
				# Found a quoted sup in the original line
				newline = re.sub("SUP '([\\w\\-]+)'", "SUP \\g<1>", i)

		# transform continuation lines with only 2 spaces
		if re.match("  +|\\t", i):
			if newline != "":
				newline = "  " + newline.strip() + "\\n"
			else:	
				newline = "  " + i.strip() + "\\n"
			
		if newline != "":
			outfile.write(newline)
		else:
			outfile.write(i)

	outfile.close()
if __name__ == "__main__":
	sys.exit(main())

Technorati Tags: , , , , , , ,

Wednesday Jan 21, 2009

OpenDS Tips: Disabling Schema checking

Opends Logo TagWhen you install OpenDS LDAP directory server, the server is configured to verify that newly-written or added entries conform to the directory server's schema (and therefore conform to the LDAP standards).

If you intend to run a reliable service with OpenDS, and provide interoperability between multiple LDAP clients, you should not change this setting. But sometime, developers need to quickly test LDAP with existing data and don't have the complete definition of the schema, or don't have time to deal with loading the proper schema. Then, the quick option is to disable schema checking.

This can be done using the dsconfig advanced mode, and the global configuration option check-schema.

dsconfig set-global-configuration-prop \\
--set check-schema:false \\
--hostname localhost \\
--trustAll \\
--port 4444 \\
--bindDN cn=Directory\\ Manager \\
--bindPassword \*\*\*\*\*\* \\
--no-prompt

There are 2 other properties that can be tuned for a finer grain control of schema checking:

  • invalid-attribute-syntax-behavior: controls whether the syntax of the attribute values are checked when adding, modifying entries.
  • single-structural-objectclass-behavior: controls how the server should behave if an attempt is made to add or modify an entry with more than one structural objectclass.

You can find more details on schema checking on the OpenDS documentation wiki and more specifically at https://www.opends.org/wiki/page/HowToExtendTheLDAPSchema#section-HowToExtendTheLDAPSchema-ConfiguringSchemaChecking

Technorati Tags: , , ,

Tuesday Jan 20, 2009

OpenDS Tips: Multiple suffixes with OpenDS

When installing OpenDS, you're asked to define a Base DN (aka Suffix, aka Naming Context) for your directory instance. A default is proposed : dc=example,dc=com.

OpenDS supports multiple suffixes per backend and multiple backends.

The Control Panel allows you to easily add suffixes to the current backend or to a new backend.

OpenDS Control Panel: New Suffix

You can also do it through the dsconfig command line tool.

$ dsconfig set-backend-prop --backend-name userRoot --add base-dn:dc=MyCompany,dc=com
--hostname localhost --port 4444 --bindDN cn=Directory\\ Manager --bindPassword \*\*\*\*\*\*\*\*
--trustAll --noPropertiesFile --no-prompt

Technorati Tags: , , ,

Friday Jan 16, 2009

OpenDS Tips: dsconfig for the complete beginners

Opends Logo TagIn a previous OpenDS Tip, I talked about dsconfig interactive mode saying that it's the default mode. When starting with OpenDS and dsconfig, my guess is that the first command tried is dsconfig --help (or -?), and then a more complete command such as dsconfig -h localhost -p 4444 ...

But the easiest way to start with dsconfig is just dsconfig with no option... The interactive mode will start querying all parameters to connect to the server before proposing a menu of configuration areas.

$ dsconfig

>>>> Specify OpenDS LDAP connection parameters

Directory server hostname or IP address [dhcp-egnb07-211-104.France.Sun.COM]:

How do you want to trust the server certificate?

1) Automatically trust
2) Use a truststore
3) Manually validate

Enter choice [3]: 1

Directory server administration port number [4444]:

Administrator user bind DN [cn=Directory Manager]:

Password for user 'cn=Directory Manager':


>>>> OpenDS configuration console main menu

What do you want to configure?

1) Access Control Handler 24) Monitor Provider
2) Account Status Notification 25) Network Group
Handler
3) Administration Connector 26) Network Group Criteria
4) Alert Handler 27) Network Group Request Filtering
Policy
5) Attribute Syntax 28) Network Group Resource Limits
6) Backend 29) Password Generator
7) Certificate Mapper 30) Password Policy
8) Connection Handler 31) Password Storage Scheme
9) Crypto Manager 32) Password Validator
10) Debug Target 33) Plugin
11) Entry Cache 34) Plugin Root
12) Extended Operation Handler 35) Replication Domain
13) Extension 36) Replication Server
14) Global Configuration 37) Root DN
15) Group Implementation 38) Root DSE Backend
16) Identity Mapper 39) SASL Mechanism Handler
17) Key Manager Provider 40) Synchronization Provider
18) Local DB Index 41) Trust Manager Provider
19) Local DB VLV Index 42) Virtual Attribute
20) Log Publisher 43) Work Queue
21) Log Retention Policy 44) Workflow
22) Log Rotation Policy 45) Workflow Element
23) Matching Rule

q) quit

Enter choice:

Also, if one of your goal is to be able to script configuration of OpenDS, use the --displayCommand option with the interactive mode: when an configuration setting is done to the server, dsconfig will display the command to use in a script to execute exactly the same configuration setting.

...
Enter choice: 14


>>>> Global Configuration management menu

What would you like to do?

1) View and edit the Global Configuration

b) back
q) quit

Enter choice [b]: 1


>>>> Configure the properties of the Global Configuration

Property Value(s)
----------------------------------------------------------------------
1) bind-with-dn-requires-password true
2) default-password-policy Default Password Policy
3) disabled-privilege If no values are defined, then
the server enforces all
privileges.
4) entry-cache-preload false
5) etime-resolution milliseconds
6) idle-time-limit 0 ms
7) lookthrough-limit 5000
8) max-allowed-client-connections 0
9) proxied-authorization-identity-mapper Exact Match
10) reject-unauthenticated-requests false
11) return-bind-error-messages false
12) save-config-on-successful-startup true
13) size-limit 1000
14) smtp-server If no values are defined, then
the server cannot send email
via SMTP.
15) time-limit 1 m
16) workflow-configuration-mode auto
17) writability-mode enabled

?) help
f) finish - apply any changes to the Global Configuration
c) cancel
q) quit

Enter choice [f]: 13


>>>> Configuring the "size-limit" property

Specifies the maximum number of entries that the Directory Server should
return to the client durin a search operation.

A value of 0 indicates that no size limit is enforced. Note that this is
the default server-wide limit, but it may be overridden on a per-user
basis using the ds-rlim-size-limit operational attribute.

Syntax: 0 <= INTEGER

Do you want to modify the "size-limit" property?

1) Keep the default value: 1000
2) Change the value

?) help
q) quit

Enter choice [1]: 2


Enter a value for the "size-limit" property [continue]: 2000

Press RETURN to continue


>>>> Configure the properties of the Global Configuration

Property Value(s)
----------------------------------------------------------------------
1) bind-with-dn-requires-password true
2) default-password-policy Default Password Policy
3) disabled-privilege If no values are defined, then
the server enforces all
privileges.
4) entry-cache-preload false
5) etime-resolution milliseconds
6) idle-time-limit 0 ms
7) lookthrough-limit 5000
8) max-allowed-client-connections 0
9) proxied-authorization-identity-mapper Exact Match
10) reject-unauthenticated-requests false
11) return-bind-error-messages false
12) save-config-on-successful-startup true
13) size-limit 2000
14) smtp-server If no values are defined, then
the server cannot send email
via SMTP.
15) time-limit 1 m
16) workflow-configuration-mode auto
17) writability-mode enabled

?) help
f) finish - apply any changes to the Global Configuration
c) cancel
q) quit

Enter choice [f]:

The Global Configuration was modified successfully

The equivalent non-interactive command-line is:
dsconfig set-global-configuration-prop \\
--set size-limit:2000 \\
--hostname dhcp-egnb07-211-104.France.Sun.COM \\
--trustAll \\
--port 4444 \\
--bindDN cn=Directory\\ Manager \\
--bindPassword \*\*\*\*\*\* \\
--no-prompt

Press RETURN to continue

Technorati Tags: , , ,

Thursday Jan 15, 2009

OpenDS Tips: Importing LDIF with encrypted passwords.

Opends Logo TagBy default, the OpenDS LDAP directory server password policy is set to reject encrypted passwords, as it cannot check that they match the quality requirements.

So when adding or importing data with encrypted passwords, the server returns some error like this:
LDAP: error code 53 - Pre-encoded passwords are not allowed for the password attribute userPassword

To allow pre-encoded passwords, the default password policy settings must be changed. This can be done using the dsconfig command line tool in advanced mode:

$ dsconfig --advanced -p 4444 -h localhost -D "cn=directory manager" -X

>>>> Specify OpenDS LDAP connection parameters

Password for user 'cn=directory manager':


>>>> OpenDS configuration console main menu

What do you want to configure?

1) Access Control Handler 24) Monitor Provider
2) Account Status Notification 25) Network Group
Handler
3) Administration Connector 26) Network Group Criteria
4) Alert Handler 27) Network Group Request Filtering
Policy
5) Attribute Syntax 28) Network Group Resource Limits
6) Backend 29) Password Generator
7) Certificate Mapper 30) Password Policy
8) Connection Handler 31) Password Storage Scheme
9) Crypto Manager 32) Password Validator
10) Debug Target 33) Plugin
11) Entry Cache 34) Plugin Root
12) Extended Operation Handler 35) Replication Domain
13) Extension 36) Replication Server
14) Global Configuration 37) Root DN
15) Group Implementation 38) Root DSE Backend
16) Identity Mapper 39) SASL Mechanism Handler
17) Key Manager Provider 40) Synchronization Provider
18) Local DB Index 41) Trust Manager Provider
19) Local DB VLV Index 42) Virtual Attribute
20) Log Publisher 43) Work Queue
21) Log Retention Policy 44) Workflow
22) Log Rotation Policy 45) Workflow Element
23) Matching Rule

q) quit

Enter choice: 30


>>>> Password Policy management menu

What would you like to do?

1) List existing Password Policies
2) Create a new Password Policy
3) View and edit an existing Password Policy
4) Delete an existing Password Policy

b) back
q) quit

Enter choice [b]: 3


>>>> Select the Password Policy from the following list:

1) Default Password Policy
2) Root Password Policy

c) cancel
q) quit

Enter choice [c]: 1


>>>> Configure the properties of the Password Policy

Property Value(s)
--------------------------------------------------------------------
1) account-status-notification-handler -
2) allow-expired-password-changes false
3) allow-multiple-password-values false
4) allow-pre-encoded-passwords false
5) allow-user-password-changes true
6) default-password-storage-scheme Salted SHA-1
7) deprecated-password-storage-scheme -
8) expire-passwords-without-warning false
9) force-change-on-add false
10) force-change-on-reset false
11) grace-login-count 0
12) idle-lockout-interval 0 s
13) last-login-time-attribute -
14) last-login-time-format -
15) lockout-duration 0 s
16) lockout-failure-count 0
17) lockout-failure-expiration-interval 0 s
18) max-password-age 0 s
19) max-password-reset-age 0 s
20) min-password-age 0 s
21) password-attribute userpassword
22) password-change-requires-current-password false
23) password-expiration-warning-interval 5 d
24) password-generator Random Password Generator
25) password-history-count 0
26) password-history-duration 0 s
27) password-validator -
28) previous-last-login-time-format -
29) require-change-by-time -
30) require-secure-authentication false
31) require-secure-password-changes false
32) skip-validation-for-administrators false
33) state-update-failure-policy reactive

?) help
f) finish - apply any changes to the Password Policy
c) cancel
q) quit

Enter choice [f]: 4


>>>> Configuring the "allow-pre-encoded-passwords" property

Indicates whether users can change their passwords by providing a
pre-encoded value.

This can cause a security risk because the clear-text version of the
password is not known and therefore validation checks cannot be applied to
it.

Do you want to modify the "allow-pre-encoded-passwords" property?

1) Keep the default value: false
2) Change it to the value: true

?) help
q) quit

Enter choice [1]: 2

Press RETURN to continue


>>>> Configure the properties of the Password Policy

Property Value(s)
--------------------------------------------------------------------
1) account-status-notification-handler -
2) allow-expired-password-changes false
3) allow-multiple-password-values false
4) allow-pre-encoded-passwords true
5) allow-user-password-changes true
6) default-password-storage-scheme Salted SHA-1
7) deprecated-password-storage-scheme -
8) expire-passwords-without-warning false
9) force-change-on-add false
10) force-change-on-reset false
11) grace-login-count 0
12) idle-lockout-interval 0 s
13) last-login-time-attribute -
14) last-login-time-format -
15) lockout-duration 0 s
16) lockout-failure-count 0
17) lockout-failure-expiration-interval 0 s
18) max-password-age 0 s
19) max-password-reset-age 0 s
20) min-password-age 0 s
21) password-attribute userpassword
22) password-change-requires-current-password false
23) password-expiration-warning-interval 5 d
24) password-generator Random Password Generator
25) password-history-count 0
26) password-history-duration 0 s
27) password-validator -
28) previous-last-login-time-format -
29) require-change-by-time -
30) require-secure-authentication false
31) require-secure-password-changes false
32) skip-validation-for-administrators false
33) state-update-failure-policy reactive

?) help
f) finish - apply any changes to the Password Policy
c) cancel
q) quit

Enter choice [f]:

The Password Policy was modified successfully

Press RETURN to continue

The equivalent non interactive command is:

$ dsconfig set-password-policy-prop \\
--policy-name "Default Password Policy" \\
--set allow-pre-encoded-passwords:true \\
--hostname localhost \\
--trustAll \\
--port 4444 \\
--bindDN "cn=directory manager" \\
--bindPassword \*\*\*\*\*\* \\
--no-prompt


Alternately, this can be done over LDAP (although it's not officially supported):

$ bin/ldapmodify -Z -X -p 4444 -h localhost -D "cn=directory manager"
Password for user 'cn=directory manager':
dn: cn=Default Password Policy,cn=Password Policies,cn=config
changetype: modify
replace: ds-cfg-allow-pre-encoded-passwords
ds-cfg-allow-pre-encoded-passwords: true

Processing MODIFY request for cn=Default Password Policy,cn=Password Policies,cn=config
MODIFY operation successful for DN cn=Default Password Policy,cn=Password Policies,cn=config

Technorati Tags: , , ,

Wednesday Jan 14, 2009

OpenDS Tips: dsconfig Advanced Mode...

Opends Logo TagOpenDS LDAP directory service comes with a command-line user interface to manage every configuration parameter of the server: dsconfig.

The dsconfig tool can be run in interactive mode (and this is the default) or non interactive mode for use in scripts. Because there are many properties, and most of them would not be changed, some properties are hidden by default.They can still be managed through dsconfig, but with the Advanced mode :

$ dsconfig --advanced

The list-properties command displays the properties sorted by category. You can filter out all properties for a given category with the --category option (-c). And have the advanced properties displayed with the --advanced option.

$ dsconfig list-properties --category global

or with the advanced properties:

$dsconfig list-properties --category global --advanced

Technorati Tags: , ,

About

This is the blog of a senior software engineer, specialized in LDAP, Directory Server and OpenDS. Ludovic Poitou works in France at the Grenoble Engineering Center, in the Directory Services Engineering team. Outside work, I love skiing and taking photo

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today