/*	$NetBSD: rbacuser.c,v 1.2 2021/08/14 16:14:53 christos Exp $	*/

/* rbacuser.c - RBAC users */
/* $OpenLDAP$ */
/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
 *
 *
 * 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>.
 */
/* ACKNOWLEDGEMENTS:
 */

#include <sys/cdefs.h>
__RCSID("$NetBSD: rbacuser.c,v 1.2 2021/08/14 16:14:53 christos Exp $");

#include "portable.h"

#include <stdio.h>

#include <ac/string.h>

#include "slap.h"
#include "slap-config.h"
#include "lutil.h"

#include "rbac.h"

static int ppolicy_cid = -1;

static rbac_user_t *
rbac_alloc_user()
{
	rbac_user_t *userp = ch_calloc( 1, sizeof(rbac_user_t) );

	BER_BVZERO( &userp->tenantid );
	BER_BVZERO( &userp->uid );
	BER_BVZERO( &userp->dn );
	BER_BVZERO( &userp->password );
	BER_BVZERO( &userp->constraints );
	BER_BVZERO( &userp->msg );
	userp->roles = NULL;
	userp->role_constraints = NULL;

	return userp;
}

static int
rbac_read_user_cb( Operation *op, SlapReply *rs )
{
	rbac_callback_info_t *cbp = op->o_callback->sc_private;
	rbac_ad_t *user_ads;
	rbac_user_t *userp = NULL;
	int rc = 0, i;

	Debug( LDAP_DEBUG_ANY, "rbac_read_user_cb\n" );

	if ( rs->sr_type != REP_SEARCH ) {
		Debug( LDAP_DEBUG_ANY, "rbac_read_user_cb: "
				"sr_type != REP_SEARCH\n" );
		return 0;
	}

	assert( cbp );

	user_ads = cbp->tenantp->schema->user_ads;

	userp = rbac_alloc_user();
	if ( !userp ) {
		Debug( LDAP_DEBUG_ANY, "rbac_read_user_cb: "
				"rbac_alloc_user failed\n" );

		goto done;
	}

	ber_dupbv( &userp->dn, &rs->sr_entry->e_name );

	Debug( LDAP_DEBUG_ANY, "DEBUG rbac_read_user_cb (%s): "
			"rc (%d)\n",
			userp->dn.bv_val, rc );

	for ( i = 0; !BER_BVISNULL( &user_ads[i].attr ); i++ ) {
		Attribute *attr = NULL;

		attr = attr_find( rs->sr_entry->e_attrs, *user_ads[i].ad );
		if ( attr != NULL ) {
			switch ( user_ads[i].type ) {
				case RBAC_ROLE_ASSIGNMENT:
					ber_bvarray_dup_x( &userp->roles, attr->a_nvals, NULL );
					break;
				case RBAC_ROLE_CONSTRAINTS:
					ber_bvarray_dup_x(
							&userp->role_constraints, attr->a_nvals, NULL );
					break;
				case RBAC_USER_CONSTRAINTS:
					ber_dupbv_x( &userp->constraints, &attr->a_nvals[0], NULL );
					break;
				case RBAC_UID:
					ber_dupbv_x( &userp->uid, &attr->a_nvals[0], NULL );
					break;
				default:
					break;
			}
		}
	}

done:;
	cbp->private = userp;

	return 0;
}

static int
rbac_bind_cb( Operation *op, SlapReply *rs )
{
	rbac_user_t *ui = op->o_callback->sc_private;

	LDAPControl *ctrl = ldap_control_find(
			LDAP_CONTROL_PASSWORDPOLICYRESPONSE, rs->sr_ctrls, NULL );
	if ( ctrl ) {
		LDAP *ld;
		ber_int_t expire, grace;
		LDAPPasswordPolicyError error;

		ldap_create( &ld );
		if ( ld ) {
			int rc = ldap_parse_passwordpolicy_control(
					ld, ctrl, &expire, &grace, &error );
			if ( rc == LDAP_SUCCESS ) {
				ui->authz = RBAC_PASSWORD_GOOD;
				if ( grace > 0 ) {
					//ui->msg.bv_len = sprintf(ui->msg.bv_val,
					//		"Password expired; %d grace logins remaining",
					//		grace);
					ui->authz = RBAC_BIND_NEW_AUTHTOK_REQD;
				} else if ( error != PP_noError ) {
					ber_str2bv( ldap_passwordpolicy_err2txt( error ), 0, 0,
							&ui->msg );

					switch ( error ) {
						case PP_passwordExpired:
							ui->authz = RBAC_PASSWORD_EXPIRATION_WARNING;

							if ( expire >= 0 ) {
								char *unit = "seconds";
								if ( expire > 60 ) {
									expire /= 60;
									unit = "minutes";
								}
								if ( expire > 60 ) {
									expire /= 60;
									unit = "hours";
								}
								if ( expire > 24 ) {
									expire /= 24;
									unit = "days";
								}
#if 0 /* Who warns about expiration so far in advance? */
								if (expire > 7) {
									expire /= 7;
									unit = "weeks";
								}
								if (expire > 4) {
									expire /= 4;
									unit = "months";
								}
								if (expire > 12) {
									expire /= 12;
									unit = "years";
								}
#endif
							}

							//rs->sr_err = ;
							break;
						case PP_accountLocked:
							ui->authz = RBAC_ACCOUNT_LOCKED;
							//rs->sr_err = ;
							break;
						case PP_changeAfterReset:
							ui->authz = RBAC_CHANGE_AFTER_RESET;
							rs->sr_err = LDAP_SUCCESS;
							break;
						case PP_passwordModNotAllowed:
							ui->authz = RBAC_NO_MODIFICATIONS;
							//rs->sr_err = ;
							break;
						case PP_mustSupplyOldPassword:
							ui->authz = RBAC_MUST_SUPPLY_OLD;
							//rs->sr_err = ;
							break;
						case PP_insufficientPasswordQuality:
							ui->authz = RBAC_INSUFFICIENT_QUALITY;
							//rs->sr_err = ;
							break;
						case PP_passwordTooShort:
							ui->authz = RBAC_PASSWORD_TOO_SHORT;
							//rs->sr_err = ;
							break;
						case PP_passwordTooYoung:
							ui->authz = RBAC_PASSWORD_TOO_YOUNG;
							//rs->sr_err = ;
							break;
						case PP_passwordInHistory:
							ui->authz = RBAC_HISTORY_VIOLATION;
							//rs->sr_err = ;
							break;
						case PP_noError:
						default:
							// do nothing
							//ui->authz = RBAC_PASSWORD_GOOD;
							rs->sr_err = LDAP_SUCCESS;
							break;
					}

//					switch (error) {
//					case PP_passwordExpired:
						/* report this during authz */
//						rs->sr_err = LDAP_SUCCESS;
						/* fallthru */
//					case PP_changeAfterReset:
//						ui->authz = RBAC_BIND_NEW_AUTHTOK_REQD;
//					}
				}
			}
			ldap_unbind_ext( ld, NULL, NULL );
		}
	}

	return 0;
}

/* exported user functions */
int
rbac_authenticate_user( Operation *op, rbac_user_t *userp )
{
	int rc = LDAP_SUCCESS;
	slap_callback cb = { 0 };
	SlapReply rs2 = { REP_RESULT };
	Operation op2 = *op;
	LDAPControl *sctrls[4];
	LDAPControl sctrl[3];
	int nsctrls = 0;
	LDAPControl c;
	struct berval ber_bvnull = BER_BVNULL;
	struct berval dn, ndn;

	rc = dnPrettyNormal( 0, &userp->dn, &dn, &ndn, NULL );
	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

	cb.sc_response = rbac_bind_cb;
	cb.sc_private = userp;
	op2.o_callback = &cb;
	op2.o_dn = ber_bvnull;
	op2.o_ndn = ber_bvnull;
	op2.o_tag = LDAP_REQ_BIND;
	op2.o_protocol = LDAP_VERSION3;
	op2.orb_method = LDAP_AUTH_SIMPLE;
	op2.orb_cred = userp->password;
	op2.o_req_dn = dn;
	op2.o_req_ndn = ndn;

	// loading the ldap pw policy controls loaded into here, added by smm:
	c.ldctl_oid = LDAP_CONTROL_PASSWORDPOLICYREQUEST;
	c.ldctl_value.bv_val = NULL;
	c.ldctl_value.bv_len = 0;
	c.ldctl_iscritical = 0;
	sctrl[nsctrls] = c;
	sctrls[nsctrls] = &sctrl[nsctrls];
	sctrls[++nsctrls] = NULL;
	op2.o_ctrls = sctrls;

	if ( ppolicy_cid < 0 ) {
		rc = slap_find_control_id( LDAP_CONTROL_PASSWORDPOLICYREQUEST,
				&ppolicy_cid );
		if ( rc != LDAP_SUCCESS ) {
			goto done;
		}
	}
	// smm - need to set the control flag too:
	op2.o_ctrlflag[ppolicy_cid] = SLAP_CONTROL_CRITICAL;

	slap_op_time( &op2.o_time, &op2.o_tincr );
	op2.o_bd = frontendDB;
	rc = op2.o_bd->be_bind( &op2, &rs2 );
	if ( userp->authz > 0 ) {
		Debug( LDAP_DEBUG_ANY, "rbac_authenticate_user (%s): "
				"password policy violation (%d)\n",
				userp->dn.bv_val ? userp->dn.bv_val : "NULL", userp->authz );
	}

done:;
	ch_free( dn.bv_val );
	ch_free( ndn.bv_val );

	Debug( LDAP_DEBUG_ANY, "rbac_authenticate_user (%s): "
			"rc (%d)\n",
			userp->dn.bv_val ? userp->dn.bv_val : "NULL", rc );
	return rc;
}

/*
	isvalidusername(): from OpenLDAP ~/contrib/slapd-modules/nssov/passwd.c
	Checks to see if the specified name is a valid user name.

    This test is based on the definition from POSIX (IEEE Std 1003.1, 2004, 3.426 User Name
	 and 3.276 Portable Filename Character Set):
	 http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_426
	 http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_276

	 The standard defines user names valid if they contain characters from
	 the set [A-Za-z0-9._-] where the hyphen should not be used as first
	 character. As an extension this test allows the dolar '$' sign as the last
	 character to support Samba special accounts.
*/
static int
isvalidusername( struct berval *bv )
{
	int i;
	char *name = bv->bv_val;
	if ( (name == NULL) || ( name[0] == '\0' ) ) return 0;
	/* check first character */
	if ( !( ( name[0] >= 'A' && name[0] <= 'Z' ) ||
				 ( name[0] >= 'a' && name[0] <= 'z' ) ||
				 ( name[0] >= '0' && name[0] <= '9' ) || name[0] == '.' ||
				 name[0] == '_' ) )
		return 0;
	/* check other characters */
	for ( i = 1; i < bv->bv_len; i++ ) {
		if ( name[i] == '$' ) {
			/* if the char is $ we require it to be the last char */
			if ( name[i + 1] != '\0' ) return 0;
		} else if ( !( ( name[i] >= 'A' && name[i] <= 'Z' ) ||
							( name[i] >= 'a' && name[i] <= 'z' ) ||
							( name[i] >= '0' && name[i] <= '9' ) ||
							name[i] == '.' || name[i] == '_' ||
							name[i] == '-' ) )
			return 0;
	}
	/* no test failed so it must be good */
	return -1;
}

rbac_user_t *
rbac_read_user( Operation *op, rbac_req_t *reqp )
{
	int rc = LDAP_SUCCESS;
	tenant_info_t *tenantp = rbac_tid2tenant( &reqp->tenantid );
	rbac_user_t *userp = NULL;
	char fbuf[RBAC_BUFLEN];
	struct berval filter = { sizeof(fbuf), fbuf };
	SlapReply rs2 = { REP_RESULT };
	Operation op2 = *op;
	slap_callback cb = { 0 };
	rbac_callback_info_t rbac_cb;

	if ( !tenantp ) {
		Debug( LDAP_DEBUG_ANY, "rbac_read_user: "
				"missing tenant information\n" );
		rc = LDAP_UNWILLING_TO_PERFORM;
		goto done;
	}

	/* uid is a pre-requisite for reading the user information */
	if ( BER_BVISNULL( &reqp->uid ) ) {
		Debug( LDAP_DEBUG_ANY, "rbac_read_user: "
				"missing uid, unable to read user entry\n" );
		rc = LDAP_UNWILLING_TO_PERFORM;
		goto done;
	}

	if ( !isvalidusername( &reqp->uid ) ) {
		Debug( LDAP_DEBUG_ANY, "rbac_read_user: "
				"invalid user id\n" );
		rc = LDAP_NO_SUCH_OBJECT;
		goto done;
	}

	rbac_cb.tenantp = tenantp;
	rbac_cb.private = NULL;

	memset( fbuf, 0, sizeof(fbuf) );
	strcpy( fbuf, "uid=" );
	strncat( fbuf, reqp->uid.bv_val, reqp->uid.bv_len );
	filter.bv_val = fbuf;
	filter.bv_len = strlen( fbuf );

	if ( rc != LDAP_SUCCESS ) {
		Debug( LDAP_DEBUG_ANY, "rbac_create_session: "
				"invalid DN syntax\n" );
		goto done;
	}

	cb.sc_private = &rbac_cb;
	cb.sc_response = rbac_read_user_cb;
	op2.o_callback = &cb;
	op2.o_tag = LDAP_REQ_SEARCH;
	op2.o_dn = tenantp->admin;
	op2.o_ndn = tenantp->admin;
	op2.o_req_dn = tenantp->users_basedn;
	op2.o_req_ndn = tenantp->users_basedn;
	op2.ors_filterstr = filter;
	op2.ors_filter = str2filter_x( &op2, filter.bv_val );
	op2.ors_scope = LDAP_SCOPE_SUBTREE;
	op2.ors_attrs = tenantp->schema->user_attrs;
	op2.ors_tlimit = SLAP_NO_LIMIT;
	op2.ors_slimit = SLAP_NO_LIMIT;
	op2.ors_attrsonly = 0;
	op2.o_bd = frontendDB;
	op2.ors_limit = NULL;
	rc = op2.o_bd->be_search( &op2, &rs2 );
	filter_free_x( &op2, op2.ors_filter, 1 );

done:;
	if ( rc == LDAP_SUCCESS && rbac_cb.private ) {
		userp = (rbac_user_t *)rbac_cb.private;
		if ( !BER_BVISNULL( &reqp->authtok ) )
			ber_dupbv( &userp->password, &reqp->authtok );
		rbac_cb.private = NULL;
		return userp;
	} else {
		userp = (rbac_user_t *)rbac_cb.private;
		rbac_free_user( userp );
		return NULL;
	}
}

/* evaluate temporal constraints for the user */
int
rbac_user_temporal_constraint( rbac_user_t *userp )
{
	int rc = LDAP_SUCCESS;
	rbac_constraint_t *cp = NULL;

	if ( BER_BVISNULL( &userp->constraints ) ) {
		/* no temporal constraint */
		goto done;
	}

	cp = rbac_bv2constraint( &userp->constraints );
	if ( !cp ) {
		Debug( LDAP_DEBUG_ANY, "rbac_user_temporal_constraint: "
				"invalid user constraint \n" );
		rc = LDAP_OTHER;
		goto done;
	}

	rc = rbac_check_time_constraint( cp );

done:;
	rbac_free_constraint( cp );

	return rc;
}

/*
rbac_constraint_t *
rbac_user_role_constraintsx(rbac_user_t *userp)
{
	rbac_constraint_t *tmp, *cp = NULL;
	int i = 0;

	if (!userp || !userp->role_constraints)
		goto done;

	while (!BER_BVISNULL(&userp->role_constraints[i])) {
		tmp = rbac_bv2constraint(&userp->role_constraints[i++]);
		if (tmp) {
			if (!cp) {
				cp = tmp;
			} else {
				tmp->next = cp;
				cp = tmp;
			}
		}
	}

done:;
	return cp;
}
*/

rbac_constraint_t *
rbac_user_role_constraints( BerVarray values )
{
	rbac_constraint_t *curr, *head = NULL;
	int i = 0;

	if ( values ) {
		while ( !BER_BVISNULL( &values[i] ) ) {
			curr = rbac_bv2constraint( &values[i++] );
			if ( curr ) {
				curr->next = head;
				head = curr;
			}
		}
	}

	return head;
}

/*

void main() {
   item * curr, * head;
   int i;

   head = NULL;

   for(i=1;i<=10;i++) {
      curr = (item *)malloc(sizeof(item));
      curr->val = i;
      curr->next  = head;
      head = curr;
   }

   curr = head;

   while(curr) {
      printf("%d\n", curr->val);
      curr = curr->next ;
   }
}

 */

/*
 *
rbac_user_role_constraints2(BerVarray values)
{
	rbac_constraint_t *tmp, *cp = NULL;
	int i = 0;

	if (!values)
		goto done;

	while (!BER_BVISNULL(&values[i])) {
		tmp = rbac_bv2constraint(&values[i++]);
		if (tmp) {
			if (!cp) {
				cp = tmp;
			} else {
				tmp->next = cp;
				cp = tmp;
				//cp->next = tmp;
				//cp = tmp->next;

			}
		}
	}

done:;
	return cp;
}


rbac_user_role_constraints3(rbac_constraint_t *values)
{
	rbac_constraint_t *tmp, *cp = NULL;
	int i = 0;

	if (!values)
		goto done;

	while (!BER_BVISNULL(values[i])) {
		tmp = rbac_bv2constraint(&values[i++]);
		if (tmp) {
			if (!cp) {
				cp = tmp;
			} else {
				tmp->next = cp;
				cp = tmp;
			}
		}
	}

done:;
	return cp;
}
*/

void
rbac_free_user( rbac_user_t *userp )
{
	if ( !userp ) return;

	if ( !BER_BVISNULL( &userp->tenantid ) ) {
		ber_memfree( userp->tenantid.bv_val );
	}

	if ( !BER_BVISNULL( &userp->uid ) ) {
		ber_memfree( userp->uid.bv_val );
	}

	if ( !BER_BVISNULL( &userp->dn ) ) {
		ber_memfree( userp->dn.bv_val );
	}

	if ( !BER_BVISNULL( &userp->constraints ) ) {
		ber_memfree( userp->constraints.bv_val );
	}

	if ( !BER_BVISNULL( &userp->password ) ) {
		ber_memfree( userp->password.bv_val );
	}

	if ( !BER_BVISNULL( &userp->msg ) ) {
		ber_memfree( userp->msg.bv_val );
	}

	if ( userp->roles ) ber_bvarray_free( userp->roles );

	if ( userp->role_constraints ) ber_bvarray_free( userp->role_constraints );

	ch_free( userp );
}