root/crypto/krb5/src/kadmin/server/ipropd_svc.c
/* -*- mode: c; c-file-style: "bsd"; indent-tabs-mode: t -*- */
/*
 * Copyright 2004 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/* #pragma ident        "@(#)ipropd_svc.c       1.2     04/02/20 SMI" */


#include "k5-platform.h"
#include <signal.h>
#include <sys/types.h>
#include <sys/resource.h> /* rlimit */
#include <syslog.h>

#include <kadm5/admin.h>
#include <kadm5/kadm_rpc.h>
#include <kadm5/server_internal.h>
#include <adm_proto.h>
#include <string.h>
#include <gssapi_krb5.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <kdb_log.h>
#include "auth.h"
#include "misc.h"
#include "osconf.h"

extern gss_name_t rqst2name(struct svc_req *rqstp);

extern void *global_server_handle;
extern int nofork;
extern short l_port;
extern char *kdb5_util;
extern char *kprop;
extern char *dump_file;
extern char *kprop_port;

static char *reply_ok_str       = "UPDATE_OK";
static char *reply_err_str      = "UPDATE_ERROR";
static char *reply_fr_str       = "UPDATE_FULL_RESYNC_NEEDED";
static char *reply_busy_str     = "UPDATE_BUSY";
static char *reply_nil_str      = "UPDATE_NIL";
static char *reply_perm_str     = "UPDATE_PERM_DENIED";
static char *reply_unknown_str  = "<UNKNOWN_CODE>";

#define LOG_UNAUTH  _("Unauthorized request: %s, client=%s, service=%s, addr=%s")
#define LOG_DONE    _("Request: %s, %s, %s, client=%s, service=%s, addr=%s")

#ifdef  DPRINT
#undef  DPRINT
#endif
#ifdef DEBUG
#define DPRINT(...)                             \
    do {                                        \
        if (nofork) {                           \
            fprintf(stderr, __VA_ARGS__);       \
            fflush(stderr);                     \
        }                                       \
    } while (0)
#else
#define DPRINT(...)
#endif

static void
debprret(char *w, update_status_t ret, kdb_sno_t sno)
{
    switch (ret) {
    case UPDATE_OK:
        printf("%s: end (OK, sno=%u)\n",
               w, sno);
        break;
    case UPDATE_ERROR:
        printf("%s: end (ERROR)\n", w);
        break;
    case UPDATE_FULL_RESYNC_NEEDED:
        printf("%s: end (FR NEEDED)\n", w);
        break;
    case UPDATE_BUSY:
        printf("%s: end (BUSY)\n", w);
        break;
    case UPDATE_NIL:
        printf("%s: end (NIL)\n", w);
        break;
    case UPDATE_PERM_DENIED:
        printf("%s: end (PERM)\n", w);
        break;
    default:
        printf("%s: end (UNKNOWN return code (%d))\n", w, ret);
    }
}

static char *
replystr(update_status_t ret)
{
    switch (ret) {
    case UPDATE_OK:
        return (reply_ok_str);
    case UPDATE_ERROR:
        return (reply_err_str);
    case UPDATE_FULL_RESYNC_NEEDED:
        return (reply_fr_str);
    case UPDATE_BUSY:
        return (reply_busy_str);
    case UPDATE_NIL:
        return (reply_nil_str);
    case UPDATE_PERM_DENIED:
        return (reply_perm_str);
    default:
        return (reply_unknown_str);
    }
}

/* Returns null on allocation failure.
   Regardless of success or failure, frees the input buffer.  */
static char *
buf_to_string(gss_buffer_desc *b)
{
    OM_uint32 min_stat;
    char *s = malloc(b->length+1);

    if (s) {
        memcpy(s, b->value, b->length);
        s[b->length] = 0;
    }
    (void) gss_release_buffer(&min_stat, b);
    return s;
}

static krb5_boolean
iprop_acl_check(krb5_context context, const char *client_name)
{
    krb5_principal client_princ;
    krb5_boolean result;

    if (krb5_parse_name(context, client_name, &client_princ) != 0)
        return FALSE;
    result = auth(context, OP_IPROP, client_princ,
                  NULL, NULL, NULL, NULL, NULL, 0);
    krb5_free_principal(context, client_princ);
    return result;
}

kdb_incr_result_t *
iprop_get_updates_1_svc(kdb_last_t *arg, struct svc_req *rqstp)
{
    static kdb_incr_result_t ret;
    char *whoami = "iprop_get_updates_1";
    int kret;
    kadm5_server_handle_t handle = global_server_handle;
    char *client_name = 0, *service_name = 0;
    char obuf[256] = {0};

    /* default return code */
    ret.ret = UPDATE_ERROR;

    DPRINT("%s: start, last_sno=%lu\n", whoami,
            (unsigned long)arg->last_sno);

    if (!handle) {
        krb5_klog_syslog(LOG_ERR,
                         _("%s: server handle is NULL"),
                         whoami);
        goto out;
    }

    {
        gss_buffer_desc client_desc, service_desc;

        if (setup_gss_names(rqstp, &client_desc, &service_desc) < 0) {
            krb5_klog_syslog(LOG_ERR,
                             _("%s: setup_gss_names failed"),
                             whoami);
            goto out;
        }
        client_name = buf_to_string(&client_desc);
        service_name = buf_to_string(&service_desc);
        if (client_name == NULL || service_name == NULL) {
            krb5_klog_syslog(LOG_ERR,
                             _("%s: out of memory recording principal names"),
                             whoami);
            goto out;
        }
    }

    DPRINT("%s: clprinc=`%s'\n\tsvcprinc=`%s'\n", whoami, client_name,
           service_name);

    if (!iprop_acl_check(handle->context, client_name)) {
        ret.ret = UPDATE_PERM_DENIED;

        DPRINT("%s: PERMISSION DENIED: clprinc=`%s'\n\tsvcprinc=`%s'\n",
                whoami, client_name, service_name);

        krb5_klog_syslog(LOG_NOTICE, LOG_UNAUTH, whoami,
                         client_name, service_name,
                         client_addr(rqstp->rq_xprt));
        goto out;
    }

    kret = ulog_get_entries(handle->context, arg, &ret);

    if (ret.ret == UPDATE_OK) {
        (void) snprintf(obuf, sizeof (obuf),
                        _("%s; Incoming SerialNo=%lu; Outgoing SerialNo=%lu"),
                        replystr(ret.ret),
                        (unsigned long)arg->last_sno,
                        (unsigned long)ret.lastentry.last_sno);
    } else {
        (void) snprintf(obuf, sizeof (obuf),
                        _("%s; Incoming SerialNo=%lu; Outgoing SerialNo=N/A"),
                        replystr(ret.ret),
                        (unsigned long)arg->last_sno);
    }

    DPRINT("%s: request %s %s\n\tclprinc=`%s'\n\tsvcprinc=`%s'\n",
           whoami, obuf,
           ((kret == 0) ? "success" : error_message(kret)),
           client_name, service_name);

    krb5_klog_syslog(LOG_NOTICE,
                     _("Request: %s, %s, %s, client=%s, service=%s, addr=%s"),
                     whoami,
                     obuf,
                     ((kret == 0) ? "success" : error_message(kret)),
                     client_name, service_name,
                     client_addr(rqstp->rq_xprt));

out:
    if (nofork)
        debprret(whoami, ret.ret, ret.lastentry.last_sno);
    free(client_name);
    free(service_name);
    return (&ret);
}


/*
 * Given a client princ (foo/fqdn@R), copy (in arg cl) the fqdn substring.
 * Return arg cl str ptr on success, else NULL.
 */
static char *
getclhoststr(const char *clprinc, char *cl, size_t len)
{
    const char *s, *e;

    if ((s = strchr(clprinc, '/')) == NULL || (e = strchr(++s, '@')) == NULL ||
        (size_t)(e - s) >= len)
        return NULL;
    memcpy(cl, s, e - s);
    cl[e - s] = '\0';
    return (cl);
}

static kdb_fullresync_result_t *
ipropx_resync(uint32_t vers, struct svc_req *rqstp)
{
    static kdb_fullresync_result_t ret;
    char *ubuf = 0;
    char clhost[NI_MAXHOST] = {0};
    int pret, fret;
    FILE *p;
    kadm5_server_handle_t handle = global_server_handle;
    char *client_name = NULL, *service_name = NULL;
    char *whoami = "iprop_full_resync_1";

    /*
     * vers contains the highest version number the client is
     * willing to accept. A client can always accept a lower
     * version: the version number is indicated in the dump
     * header.
     */

    /* default return code */
    ret.ret = UPDATE_ERROR;

    if (!handle) {
        krb5_klog_syslog(LOG_ERR,
                         _("%s: server handle is NULL"),
                         whoami);
        goto out;
    }

    DPRINT("%s: start\n", whoami);

    {
        gss_buffer_desc client_desc, service_desc;

        if (setup_gss_names(rqstp, &client_desc, &service_desc) < 0) {
            DPRINT("%s: setup_gss_names failed\n", whoami);
            krb5_klog_syslog(LOG_ERR,
                             _("%s: setup_gss_names failed"),
                             whoami);
            goto out;
        }
        client_name = buf_to_string(&client_desc);
        service_name = buf_to_string(&service_desc);
        if (client_name == NULL || service_name == NULL) {
            DPRINT("%s: out of memory\n", whoami);
            krb5_klog_syslog(LOG_ERR,
                             _("%s: out of memory recording principal names"),
                             whoami);
            goto out;
        }
    }

    DPRINT("%s: clprinc=`%s'\n\tsvcprinc=`%s'\n",
            whoami, client_name, service_name);

    if (!iprop_acl_check(handle->context, client_name)) {
        ret.ret = UPDATE_PERM_DENIED;

        DPRINT("%s: Permission denied\n", whoami);
        krb5_klog_syslog(LOG_NOTICE, LOG_UNAUTH, whoami,
                         client_name, service_name,
                         client_addr(rqstp->rq_xprt));
        goto out;
    }

    if (!getclhoststr(client_name, clhost, sizeof (clhost))) {
        krb5_klog_syslog(LOG_ERR,
                         _("%s: getclhoststr failed"),
                         whoami);
        goto out;
    }

    /*
     * Note the -i; modified version of kdb5_util dump format
     * to include sno (serial number). This argument is now
     * versioned (-i0 for legacy dump format, -i1 for ipropx
     * version 1 format, etc).
     *
     * The -c option ("conditional") causes the dump to dump only if no
     * dump already exists or that dump is not in ipropx format, or the
     * sno and timestamp in the header of that dump are outside the
     * ulog.  This allows us to share a single global dump with all
     * replicas, since it's OK to share an older dump, as long as its
     * sno and timestamp are in the ulog (then the replicas can get the
     * subsequent updates very iprop).
     */
    if (asprintf(&ubuf, "%s -r %s dump -i%d -c %s", kdb5_util,
                 handle->params.realm, vers, dump_file) < 0) {
        krb5_klog_syslog(LOG_ERR,
                         _("%s: cannot construct kdb5 util dump string too long; out of memory"),
                         whoami);
        goto out;
    }

    /*
     * Fork to dump the db and xfer it to the replica.
     * (the fork allows parent to return quickly and the child
     * acts like a callback to the replica).
     */
    fret = fork();
    DPRINT("%s: fork=%d (%d)\n", whoami, fret, getpid());

    switch (fret) {
    case -1: /* error */
        if (nofork) {
            perror(whoami);
        }
        DPRINT("%s: fork failed\n", whoami);
        krb5_klog_syslog(LOG_ERR,
                         _("%s: fork failed: %s"),
                         whoami,
                         error_message(errno));
        goto out;

    case 0: /* child */
        DPRINT("%s: run `%s' ...\n", whoami, ubuf);
        (void) signal(SIGCHLD, SIG_DFL);
        /* run kdb5_util(1M) dump for IProp */
        p = popen(ubuf, "w");
        if (p == NULL) {
            krb5_klog_syslog(LOG_ERR,
                             _("%s: popen failed: %s"),
                             whoami, error_message(errno));
            _exit(1);
        }
        pret = pclose(p);
        DPRINT("%s: pclose=%d\n", whoami, pret);
        if (pret != 0) {
            /* XXX popen/pclose may not set errno
               properly, and the error could be from the
               subprocess anyways.  */
            if (nofork) {
                perror(whoami);
            }
            krb5_klog_syslog(LOG_ERR,
                             _("%s: pclose(popen) failed: %s"),
                             whoami,
                             error_message(errno));
            _exit(1);
        }

        if (kprop_port != NULL) {
            DPRINT("%s: exec `kprop -r %s -f %s -P %s %s' ...\n",
                   whoami, handle->params.realm, dump_file, kprop_port,
                   clhost);
            pret = execl(kprop, "kprop", "-r", handle->params.realm, "-f",
                         dump_file, "-P", kprop_port, clhost, NULL);
        } else {
            DPRINT("%s: exec `kprop -r %s -f %s %s' ...\n",
                   whoami, handle->params.realm, dump_file, clhost);
            pret = execl(kprop, "kprop", "-r", handle->params.realm, "-f",
                         dump_file, clhost, NULL);
        }
        perror(whoami);
        krb5_klog_syslog(LOG_ERR,
                         _("%s: exec failed: %s"),
                         whoami,
                         error_message(errno));
        _exit(1);

    default: /* parent */
        ret.ret = UPDATE_OK;
        /* not used by replica (sno is retrieved from kdb5_util dump) */
        ret.lastentry.last_sno = 0;
        ret.lastentry.last_time.seconds = 0;
        ret.lastentry.last_time.useconds = 0;

        DPRINT("%s: spawned resync process %d, client=%s, "
                "service=%s, addr=%s\n", whoami, fret, client_name,
                service_name, client_addr(rqstp->rq_xprt));
        krb5_klog_syslog(LOG_NOTICE,
                         _("Request: %s, spawned resync process %d, client=%s, service=%s, addr=%s"),
                         whoami, fret,
                         client_name, service_name,
                         client_addr(rqstp->rq_xprt));

        goto out;
    }

out:
    if (nofork)
        debprret(whoami, ret.ret, 0);
    free(client_name);
    free(service_name);
    free(ubuf);
    return (&ret);
}

kdb_fullresync_result_t *
iprop_full_resync_1_svc(/* LINTED */ void *argp, struct svc_req *rqstp)
{
    return ipropx_resync(IPROPX_VERSION_0, rqstp);
}

kdb_fullresync_result_t *
iprop_full_resync_ext_1_svc(uint32_t *argp, struct svc_req *rqstp)
{
    return ipropx_resync(*argp, rqstp);
}

static int
check_iprop_rpcsec_auth(struct svc_req *rqstp)
{
    /* XXX Since the client can authenticate against any principal in
       the database, we need to do a sanity check.  Only checking for
       "kiprop" now, but that means theoretically the client could be
       authenticating to kiprop on some other machine.  */
    /* Code taken from kadm_rpc_svc.c, tweaked.  */

     gss_ctx_id_t ctx;
     krb5_context kctx;
     OM_uint32 maj_stat, min_stat;
     gss_name_t name;
     krb5_principal princ;
     int ret, success;
     krb5_data *c1, *realm;
     gss_buffer_desc gss_str;
     kadm5_server_handle_t handle;
     size_t slen;
     char *sdots;

     success = 0;
     handle = (kadm5_server_handle_t)global_server_handle;

     if (rqstp->rq_cred.oa_flavor != RPCSEC_GSS)
          return 0;

     ctx = rqstp->rq_svccred;

     maj_stat = gss_inquire_context(&min_stat, ctx, NULL, &name,
                                    NULL, NULL, NULL, NULL, NULL);
     if (maj_stat != GSS_S_COMPLETE) {
          krb5_klog_syslog(LOG_ERR,
                           _("check_rpcsec_auth: failed inquire_context, "
                             "stat=%u"), maj_stat);
          log_badauth(maj_stat, min_stat, rqstp->rq_xprt, NULL);
          goto fail_name;
     }

     kctx = handle->context;
     ret = gss_to_krb5_name_1(rqstp, kctx, name, &princ, &gss_str);
     if (ret == 0)
          goto fail_name;

     slen = gss_str.length;
     trunc_name(&slen, &sdots);
     /*
      * Since we accept with GSS_C_NO_NAME, the client can authenticate
      * against the entire kdb.  Therefore, ensure that the service
      * name is something reasonable.
      */
     if (krb5_princ_size(kctx, princ) != 2)
          goto fail_princ;

     c1 = krb5_princ_component(kctx, princ, 0);
     realm = krb5_princ_realm(kctx, princ);
     if (strncmp(handle->params.realm, realm->data, realm->length) == 0
         && strncmp("kiprop", c1->data, c1->length) == 0) {
         success = 1;
     }

fail_princ:
     if (!success) {
          krb5_klog_syslog(LOG_ERR, _("bad service principal %.*s%s"),
                           (int) slen, (char *) gss_str.value, sdots);
     }
     gss_release_buffer(&min_stat, &gss_str);
     krb5_free_principal(kctx, princ);
fail_name:
     gss_release_name(&min_stat, &name);
     return success;
}

void
krb5_iprop_prog_1(struct svc_req *rqstp,
                  SVCXPRT *transp)
{
    union {
        kdb_last_t iprop_get_updates_1_arg;
    } argument;
    void *result;
    xdrproc_t _xdr_argument, _xdr_result;
    void *(*local)(char *, struct svc_req *);
    char *whoami = "krb5_iprop_prog_1";

    if (!check_iprop_rpcsec_auth(rqstp)) {
        krb5_klog_syslog(LOG_ERR, _("authentication attempt failed: %s, RPC "
                                    "authentication flavor %d"),
                         client_addr(rqstp->rq_xprt),
                         rqstp->rq_cred.oa_flavor);
        svcerr_weakauth(transp);
        return;
    }

    switch (rqstp->rq_proc) {
    case NULLPROC:
        (void) svc_sendreply(transp, xdr_void,
                             (char *)NULL);
        return;

    case IPROP_GET_UPDATES:
        _xdr_argument = (xdrproc_t)xdr_kdb_last_t;
        _xdr_result = (xdrproc_t)xdr_kdb_incr_result_t;
        local = (void *(*)(char *, struct svc_req *))iprop_get_updates_1_svc;
        break;

    case IPROP_FULL_RESYNC:
        _xdr_argument = (xdrproc_t)xdr_void;
        _xdr_result = (xdrproc_t)xdr_kdb_fullresync_result_t;
        local = (void *(*)(char *, struct svc_req *))iprop_full_resync_1_svc;
        break;

    case IPROP_FULL_RESYNC_EXT:
        _xdr_argument = (xdrproc_t)xdr_u_int32;
        _xdr_result = (xdrproc_t)xdr_kdb_fullresync_result_t;
        local = (void *(*)(char *, struct svc_req *))iprop_full_resync_ext_1_svc;
        break;

    default:
        krb5_klog_syslog(LOG_ERR,
                         _("RPC unknown request: %d (%s)"),
                         rqstp->rq_proc, whoami);
        svcerr_noproc(transp);
        return;
    }
    (void) memset(&argument, 0, sizeof (argument));
    if (!svc_getargs(transp, _xdr_argument, (caddr_t)&argument)) {
        krb5_klog_syslog(LOG_ERR,
                         _("RPC svc_getargs failed (%s)"),
                         whoami);
        svcerr_decode(transp);
        return;
    }
    result = (*local)((char *)&argument, rqstp);

    if (_xdr_result && result != NULL &&
        !svc_sendreply(transp, _xdr_result, result)) {
        krb5_klog_syslog(LOG_ERR,
                         _("RPC svc_sendreply failed (%s)"),
                         whoami);
        svcerr_systemerr(transp);
    }
    if (!svc_freeargs(transp, _xdr_argument, (caddr_t)&argument)) {
        krb5_klog_syslog(LOG_ERR,
                         _("RPC svc_freeargs failed (%s)"),
                         whoami);

        exit(1);
    }

    if (rqstp->rq_proc == IPROP_GET_UPDATES) {
        /* LINTED */
        kdb_incr_result_t *r = (kdb_incr_result_t *)result;

        if (r->ret == UPDATE_OK) {
            ulog_free_entries(r->updates.kdb_ulog_t_val,
                              r->updates.kdb_ulog_t_len);
            r->updates.kdb_ulog_t_val = NULL;
            r->updates.kdb_ulog_t_len = 0;
        }
    }

}