/* Authors: Rob Crittenden <rcritten@redhat.com>
 *
 * Copyright (C) 2009  Red Hat
 * see file 'COPYING' for use and warranty information
 *
 * This program is free software you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <krb5.h>
#include <popt.h>
#include <errno.h>

#include "ipa-client-common.h"
#include "config.h"

#define KERBEROS_ERROR	1
#define OOM_ERROR		2
#define KEYTAB_ERROR	3
#define PRINCIPAL_ERROR	4
#define NOT_FOUND		5
#define REMOVE_ERROR	6
#define CURSOR_ERROR	7

int
remove_principal(krb5_context context, krb5_keytab ktid, const char *principal, int debug)
{
    krb5_error_code krberr;
    krb5_keytab_entry entry, entry2;
    int rval = 0;
    int removed = 0;

    memset(&entry, 0, sizeof(entry));
    krberr = krb5_parse_name(context, principal, &entry.principal);
    if (krberr) {
        fprintf(stderr, _("Unable to parse principal name\n"));
        if (debug)
            fprintf(stderr, _("krb5_parse_name %1$d: %2$s\n"),
                            krberr, error_message(krberr));
        rval = PRINCIPAL_ERROR;
        goto done;
    }

    /* Loop through the keytab and remove all entries with this principal name
     * irrespective of the encryption type. A failure to find one after the
     * first means we're done.
     */
    fprintf(stderr, _("Removing principal %s\n"), principal);
    while (1) {
        memset(&entry2, 0, sizeof(entry2));
        krberr = krb5_kt_get_entry(context, ktid,
                                entry.principal,
                                0,
                                0,
                                &entry2);
        if (krberr) {
            if (removed > 0)
                /* not found but we've removed some, we're done */
                break;
            if (krberr == ENOENT) {
                fprintf(stderr, _("Failed to open keytab\n"));
                rval = KEYTAB_ERROR;
                goto done;
            }
            fprintf(stderr, _("principal not found\n"));
            if (debug)
                fprintf(stderr, _("krb5_kt_get_entry %1$d: %2$s\n"),
                                krberr, error_message(krberr));
            rval = NOT_FOUND;
            break;
        }

        krberr = krb5_kt_remove_entry(context, ktid, &entry2);
        if (krberr) {
            fprintf(stderr, _("Unable to remove entry\n"));
            if (debug) {
                fprintf(stdout, _("kvno %d\n"), entry2.vno);
                fprintf(stderr, _("krb5_kt_remove_entry %1$d: %2$s\n"),
                                krberr, error_message(krberr));
            }
            rval = 6;
            break;
        }

        krb5_free_keytab_entry_contents(context, &entry2);
        removed++;
    }

    if (entry2.principal)
        krb5_free_keytab_entry_contents(context, &entry2);

done:

    return rval;
}

int
remove_realm(krb5_context context, krb5_keytab ktid, const char *realm, int debug)
{
    krb5_error_code krberr;
    krb5_keytab_entry entry;
    krb5_kt_cursor kt_cursor;
    char * entry_princ_s = NULL;
    int rval = 0;
    bool realm_found = false;

    krberr = krb5_kt_start_seq_get(context, ktid, &kt_cursor);
    if (krberr) {
        fprintf(stderr, _("Failed to set cursor '%1$s'\n"),
                error_message(krberr));
        rval = CURSOR_ERROR;
        goto done;
    }
    memset(&entry, 0, sizeof(entry));
    while (krb5_kt_next_entry(context, ktid, &entry, &kt_cursor) == 0) {
        krberr = krb5_unparse_name(context, entry.principal, &entry_princ_s);
        if (krberr) {
            fprintf(stderr, _("Unable to parse principal\n"));
            if (debug) {
                fprintf(stderr, _("krb5_unparse_name %1$d: %2$s\n"),
                                krberr, error_message(krberr));
            }
            rval = PRINCIPAL_ERROR;
            goto done;
        }

        /* keytab entries are locked when looping. Temporarily suspend
         * the looping. */
        krberr = krb5_kt_end_seq_get(context, ktid, &kt_cursor);
        if (krberr) {
            fprintf(stderr, _("Failed to set cursor '%1$s'\n"),
                    error_message(krberr));
            rval = CURSOR_ERROR;
            goto done;
        }

        if (strstr(entry_princ_s, realm) != NULL) {
            realm_found = true;
            rval = remove_principal(context, ktid, entry_princ_s, debug);
            if (rval != 0)
                goto done;
            /* Have to reset the cursor */
            krberr = krb5_kt_start_seq_get(context, ktid, &kt_cursor);
            if (krberr) {
                fprintf(stderr, _("Failed to set cursor '%1$s'\n"),
                        error_message(krberr));
                rval = CURSOR_ERROR;
                goto done;
            }
        }
    }

    if (!realm_found) {
        fprintf(stderr, _("realm not found\n"));
        return 5;
    }

done:

    return rval;
}

int
main(int argc, const char **argv)
{
    krb5_context context;
    krb5_error_code krberr;
    krb5_keytab ktid;
    krb5_kt_cursor cursor;
    char * ktname = NULL;
    char * atrealm = NULL;
    poptContext pc;
    static const char *keytab = NULL;
    static const char *principal = NULL;
    static const char *realm = NULL;
    int debug = 0;
    int ret, rval = 0;
    struct poptOption options[] = {
        { "debug", 'd', POPT_ARG_NONE, &debug, 0,
          _("Print debugging information"), _("Debugging output") },
        { "principal", 'p', POPT_ARG_STRING, &principal, 0,
          _("The principal to remove from the keytab (ex: ftp/ftp.example.com@EXAMPLE.COM)"),
          _("Kerberos Service Principal Name") },
        { "keytab", 'k', POPT_ARG_STRING, &keytab, 0,
          _("The keytab file to remove the principcal(s) from"), _("Keytab File Name") },
        { "realm", 'r', POPT_ARG_STRING, &realm, 0,
          _("Remove all principals in this realm"), _("Realm name") },
        POPT_AUTOHELP
        POPT_TABLEEND
    };

    ret = init_gettext();
    if (ret) {
        fprintf(stderr, "Failed to load translations\n");
    }

    memset(&ktid, 0, sizeof(ktid));

    krberr = krb5_init_context(&context);
    if (krberr) {
        fprintf(stderr, _("Kerberos context initialization failed\n"));
        exit(1);
    }

    pc = poptGetContext("ipa-rmkeytab", argc, (const char **)argv, options, 0);
    ret = poptGetNextOpt(pc);
    if (ret != -1 || (!principal && !realm) || !keytab) {
        poptPrintUsage(pc, stderr, 0);
        rval = KERBEROS_ERROR;
        goto cleanup;
    }

    ret = asprintf(&ktname, "WRFILE:%s", keytab);
    if (ret == -1) {
        rval = OOM_ERROR;
        goto cleanup;
    }

    /* The remove_realm function just does a substring match. Ensure that
     * the string we pass in looks like a realm.
     */
    if (realm) {
        if (realm[0] != '@') {
            ret = asprintf(&atrealm, "@%s", realm);
            if (ret == -1) {
                rval = OOM_ERROR;
                goto cleanup;
            }
        } else {
            atrealm = strdup(realm);

            if (NULL == atrealm) {
                rval = OOM_ERROR;
                goto cleanup;
            }
        }
    }

    krberr = krb5_kt_resolve(context, ktname, &ktid);
    if (krberr) {
        fprintf(stderr, _("Failed to open keytab '%1$s': %2$s\n"), keytab,
            error_message(krberr));
        rval = KEYTAB_ERROR;
        goto cleanup;
    }
    krberr = krb5_kt_start_seq_get(context, ktid, &cursor);
    if (krberr) {
        fprintf(stderr, _("Failed to set cursor '%1$s'\n"),
            error_message(krberr));
        rval = CURSOR_ERROR;
        goto cleanup;
    }
    krb5_kt_end_seq_get(context, ktid, &cursor);

    if (principal)
        rval = remove_principal(context, ktid, principal, debug);
    else if (realm)
        rval = remove_realm(context, ktid, atrealm, debug);

cleanup:
    if (ktid) {
        krberr = krb5_kt_close(context, ktid);
        if (krberr) {
            fprintf(stderr, _("Closing keytab failed\n"));
            if (debug)
                fprintf(stderr, _("krb5_kt_close %1$d: %2$s\n"),
                                krberr, error_message(krberr));
        }
    }

    krb5_free_context(context);

    poptFreeContext(pc);

    free(atrealm);
    free(ktname);

    return rval;
}
