/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
 *
 * Copyright 2011 PADL Software Pty Ltd.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted only as authorized by the OpenLDAP
 * Public License.
 *
 * A copy of this license is available in the file LICENSE in the
 * top-level directory of the distribution or, alternatively, at
 * <http://www.OpenLDAP.org/license.html>.
 */

#include <portable.h>

#include <ac/string.h>
#include <slap.h>
#include <lutil.h>

#include <sasl/sasl.h>
#include <gssapi/gssapi.h>
#include <gssapi/gssapi_ext.h>

#define ACL_BUF_SIZE 1024

typedef struct gssattr_t {
	slap_style_t		gssattr_style;
	struct berval		gssattr_name;		/* asserted name */
	struct berval		gssattr_value;		/* asserted value */
} gssattr_t;

static int gssattr_dynacl_destroy( void *priv );

static int
regex_matches(
	struct berval	*pat,		/* pattern to expand and match against */
	char		*str,		/* string to match against pattern */
	struct berval	*dn_matches,	/* buffer with $N expansion variables from DN */
	struct berval	*val_matches,	/* buffer with $N expansion variables from val */
	AclRegexMatches	*matches	/* offsets in buffer for $N expansion variables */
);

static int
gssattr_dynacl_parse(
	const char	*fname,
	int 		lineno,
	const char	*opts,
	slap_style_t	style,
	const char	*pattern,
	void		**privp )
{
	gssattr_t	*gssattr;

	gssattr = (gssattr_t *)ch_calloc( 1, sizeof( gssattr_t ) );

	if ( opts == NULL || opts[0] == '\0' ) {
		fprintf( stderr, "%s line %d: GSS ACL: no attribute specified.\n",
			 fname, lineno );
		goto cleanup;
	}

	if ( pattern == NULL || pattern[0] == '\0' ) {
		fprintf( stderr, "%s line %d: GSS ACL: no attribute value specified.\n",
			 fname, lineno );
		goto cleanup;
	}

	gssattr->gssattr_style = style;

	switch ( gssattr->gssattr_style ) {
	case ACL_STYLE_BASE:
	case ACL_STYLE_REGEX:
	case ACL_STYLE_EXPAND:
		break;
	default:
		fprintf( stderr, "%s line %d: GSS ACL: unsupported style \"%s\".\n",
			 fname, lineno, style_strings[style] );
		goto cleanup;
		break;
	}

	ber_str2bv( opts,    0, 1, &gssattr->gssattr_name );
	ber_str2bv( pattern, 0, 1, &gssattr->gssattr_value );

	*privp = (void *)gssattr;
	return 0;

cleanup:
	(void)gssattr_dynacl_destroy( (void *)gssattr );

	return 1;
}

static int
gssattr_dynacl_unparse(
	void		*priv,
	struct berval	*bv )
{
	gssattr_t	*gssattr = (gssattr_t *)priv;
	char		*ptr;

	bv->bv_len = STRLENOF( " dynacl/gss/.expand=" ) +
		     gssattr->gssattr_name.bv_len +
		     gssattr->gssattr_value.bv_len;
	bv->bv_val = ch_malloc( bv->bv_len + 1 );

	ptr = lutil_strcopy( bv->bv_val, " dynacl/gss/" );
	ptr = lutil_strncopy( ptr, gssattr->gssattr_name.bv_val,
			      gssattr->gssattr_name.bv_len );
	switch ( gssattr->gssattr_style ) {
	case ACL_STYLE_BASE:
		ptr = lutil_strcopy( ptr, ".exact=" );
		break;
	case ACL_STYLE_REGEX:
		ptr = lutil_strcopy( ptr, ".regex=" );
		break;
	case ACL_STYLE_EXPAND:
		ptr = lutil_strcopy( ptr, ".expand=" );
		break;
	default:
		assert( 0 );
		break;
	}

	ptr = lutil_strncopy( ptr, gssattr->gssattr_value.bv_val,
			      gssattr->gssattr_value.bv_len );

	ptr[ 0 ] = '\0';

	bv->bv_len = ptr - bv->bv_val;

	return 0;
}

static int
gssattr_dynacl_mask(
	void			*priv,
	Operation		*op,
	Entry			*target,
	AttributeDescription	*desc,
	struct berval		*val,
	int			nmatch,
	regmatch_t		*matches,
	slap_access_t		*grant,
	slap_access_t		*deny )
{
	gssattr_t	*gssattr = (gssattr_t *)priv;
	sasl_conn_t	*sasl_ctx = op->o_conn->c_sasl_authctx;
	gss_name_t	gss_name = GSS_C_NO_NAME;
	OM_uint32	major, minor;
	int		more = -1;
	int		authenticated, complete;
	gss_buffer_desc	attr = GSS_C_EMPTY_BUFFER;
	int		granted = 0;

	ACL_INVALIDATE( *deny );

	if ( sasl_ctx == NULL ||
	     sasl_getprop( sasl_ctx, SASL_GSS_PEER_NAME, (const void **)&gss_name) != 0 ||
	     gss_name == GSS_C_NO_NAME ) {
		return 0;
	}

	attr.length = gssattr->gssattr_name.bv_len;
	attr.value = gssattr->gssattr_name.bv_val;

	while ( more != 0 ) {
		AclRegexMatches amatches = { 0 };
		gss_buffer_desc	gss_value = GSS_C_EMPTY_BUFFER;
		gss_buffer_desc	gss_display_value = GSS_C_EMPTY_BUFFER;
		struct berval bv_value;

		major = gss_get_name_attribute( &minor, gss_name, &attr,
						&authenticated, &complete,
						&gss_value, &gss_display_value, &more );
		if ( GSS_ERROR( major ) ) {
			break;
		} else if ( authenticated == 0 ) {
			gss_release_buffer( &minor, &gss_value );
			gss_release_buffer( &minor, &gss_display_value );
			continue;
		}

		bv_value.bv_len = gss_value.length;
		bv_value.bv_val = (char *)gss_value.value;

		if ( !ber_bvccmp( &gssattr->gssattr_value, '*' ) ) {
			if ( gssattr->gssattr_style != ACL_STYLE_BASE ) {
				amatches.dn_count = nmatch;
				AC_MEMCPY( amatches.dn_data, matches, sizeof( amatches.dn_data ) );
			}

			switch ( gssattr->gssattr_style ) {
			case ACL_STYLE_REGEX:
				/* XXX assumes value NUL terminated */
				granted = regex_matches( &gssattr->gssattr_value, bv_value.bv_val,
							  &target->e_nname, val, &amatches );
				break;
			case ACL_STYLE_EXPAND: {
				struct berval bv;
				char buf[ACL_BUF_SIZE];

				bv.bv_len = sizeof( buf ) - 1;
				bv.bv_val = buf;

				granted = ( acl_string_expand( &bv, &gssattr->gssattr_value,
							       &target->e_nname, val,
							       &amatches ) == 0 ) &&
					  ( ber_bvstrcmp( &bv, &bv_value) == 0 );
				break;
			}
			case ACL_STYLE_BASE:
				granted = ( ber_bvstrcmp( &gssattr->gssattr_value, &bv_value ) == 0 );
				break;
			default:
				assert(0);
				break;
			}
		} else {
			granted = 1;
		}

		gss_release_buffer( &minor, &gss_value );
		gss_release_buffer( &minor, &gss_display_value );

		if ( granted ) {
			break;
		}
	}

	if ( granted ) {
		ACL_LVL_ASSIGN_WRITE( *grant );
	}

	return 0;
}

static int
gssattr_dynacl_destroy(
	void		*priv )
{
	gssattr_t		*gssattr = (gssattr_t *)priv;

	if ( gssattr != NULL ) {
		if ( !BER_BVISNULL( &gssattr->gssattr_name ) ) {
			ber_memfree( gssattr->gssattr_name.bv_val );
		}
		if ( !BER_BVISNULL( &gssattr->gssattr_value ) ) {
			ber_memfree( gssattr->gssattr_value.bv_val );
		}
		ch_free( gssattr );
	}

	return 0;
}

static struct slap_dynacl_t gssattr_dynacl = {
	"gss",
	gssattr_dynacl_parse,
	gssattr_dynacl_unparse,
	gssattr_dynacl_mask,
	gssattr_dynacl_destroy
};

int
init_module( int argc, char *argv[] )
{
	return slap_dynacl_register( &gssattr_dynacl );
}


static int
regex_matches(
	struct berval	*pat,		/* pattern to expand and match against */
	char		*str,		/* string to match against pattern */
	struct berval	*dn_matches,	/* buffer with $N expansion variables from DN */
	struct berval	*val_matches,	/* buffer with $N expansion variables from val */
	AclRegexMatches	*matches	/* offsets in buffer for $N expansion variables */
)
{
	regex_t re;
	char newbuf[ACL_BUF_SIZE];
	struct berval bv;
	int	rc;

	bv.bv_len = sizeof( newbuf ) - 1;
	bv.bv_val = newbuf;

	if (str == NULL) {
		str = "";
	};

	acl_string_expand( &bv, pat, dn_matches, val_matches, matches );
	rc = regcomp( &re, newbuf, REG_EXTENDED|REG_ICASE );
	if ( rc ) {
		char error[ACL_BUF_SIZE];
		regerror( rc, &re, error, sizeof( error ) );

		Debug( LDAP_DEBUG_TRACE,
		    "compile( \"%s\", \"%s\") failed %s\n",
			pat->bv_val, str, error );
		return( 0 );
	}

	rc = regexec( &re, str, 0, NULL, 0 );
	regfree( &re );

	Debug( LDAP_DEBUG_TRACE,
	    "=> regex_matches: string:	 %s\n", str );
	Debug( LDAP_DEBUG_TRACE,
	    "=> regex_matches: rc: %d %s\n",
		rc, !rc ? "matches" : "no matches" );
	return( !rc );
}

