From 119dd0e09dd46ba90781f09fd96afc0f39e05b74 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slaznick@redhat.com>
Date: Fri, 21 Aug 2015 09:37:13 +0200
Subject: [PATCH 4/6] IPA: Added the 'repeat' keyword functionality

Improvements for IPA provider - HBAC time policies:

The 'repeat' time rules language keyword is supposed to simulate
the behavior of recurrence rules as seen in iCalendar.

https://fedorahosted.org/freeipa/ticket/547
---
 src/providers/ipa/ipa_timerules.c | 515 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 491 insertions(+), 24 deletions(-)

diff --git a/src/providers/ipa/ipa_timerules.c b/src/providers/ipa/ipa_timerules.c
index 44766c063b142dbc69bfac875a04ad948e6ae5f2..dc25c20caac15ef656d45d138df64cdff40ae97d 100644
--- a/src/providers/ipa/ipa_timerules.c
+++ b/src/providers/ipa/ipa_timerules.c
@@ -22,6 +22,8 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+#define _XOPEN_SOURCE 600
+
 #include <errno.h>
 #include <stdlib.h>
 #include <string.h>
@@ -40,6 +42,10 @@
 #define WOY_STR "weekofyear"
 #define MOY_STR "monthofyear"
 #define YEAR_STR "year"
+#define RPT_STR "repeat"
+
+/* 60sec * 60min * 24hours */
+#define DAY_SECS 86400
 
 #define isleap(y) ((((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0)) ? true : false)
 
@@ -67,12 +73,21 @@ enum time_types {
     WOY,
     MOY,
     YEAR,
+    RPT,
     START,
     END,
     VAL_ERR,
     MEM_ERR
 };
 
+enum repeat_type {
+    NONE = 0,
+    DAILY,
+    WEEKLY,
+    MONTHLY,
+    YEARLY,
+};
+
 struct time_token {
     enum time_types type;
     int value_low;
@@ -80,12 +95,20 @@ struct time_token {
     int pos;
 };
 
+struct repeat {
+    struct tm *startdate;
+    struct tm *enddate;
+    int interval;
+    enum repeat_type type;
+};
+
 errno_t
 eval_time_rule(const char *rule,
                  struct tm* cmp_time,
+                 const char *tz,
                  bool *matched);
 
-struct tm *time_to_timezone(time_t t, const char *tz);
+struct tm *time_to_timezone(time_t t, const char *tz, struct tm **result);
 
 errno_t
 hbac_evaluate_time_rules(struct hbac_time_rules *rule,
@@ -94,8 +117,10 @@ hbac_evaluate_time_rules(struct hbac_time_rules *rule,
 {
     errno_t ret;
     struct tm *cmp_time;
+    struct tm *tmp_time;
     int i;
 
+
     /* No access time policies = always match */
     if(!(rule->accesstimes && rule->accesstimes[0]) &&
         !(rule->exceptions && rule->exceptions[0])) {
@@ -103,13 +128,20 @@ hbac_evaluate_time_rules(struct hbac_time_rules *rule,
         return EOK;
     }
 
+    if((cmp_time = malloc(sizeof(struct tm))) == NULL) {
+        return MEM_ERR;
+    }
+
     if(!rule->timezone || strcasecmp(rule->timezone, "utc") == 0)
-        cmp_time = gmtime(&req_time);
+        tmp_time = gmtime_r(&req_time, cmp_time);
     else if(strcasecmp(rule->timezone, "host") == 0)
-        cmp_time = localtime(&req_time);
-    else cmp_time = time_to_timezone(req_time, rule->timezone);
+        tmp_time = localtime_r(&req_time, cmp_time);
+    else tmp_time = time_to_timezone(req_time, rule->timezone, &cmp_time);
 
-    if(cmp_time == NULL) return ENOMATCH;
+    if(tmp_time == NULL) {
+        free(cmp_time);
+        return ENOMATCH;
+    }
 
     /* conversion to our language */
     if(cmp_time->tm_wday == 0)
@@ -120,16 +152,16 @@ hbac_evaluate_time_rules(struct hbac_time_rules *rule,
 
     if(rule->exceptions) {
         for(i = 0; rule->exceptions[i]; i++) {
-            ret = eval_time_rule(rule->exceptions[i], cmp_time, matched);
+            ret = eval_time_rule(rule->exceptions[i], cmp_time,
+                                 rule->timezone, matched);
             if(ret != EOK) {
-                /*DEBUG(SSSDBG_CONF_SETTINGS,
-                      "Invalid value found in accessTimeExclude rule [%s]\n",
-                      rule->exceptions[i]);*/
+                free(cmp_time);
                 *matched = false;
                 return ENOMATCH;
             }
             /* exception matched, rule can't be applied */
             if(*matched == true) {
+                free(cmp_time);
                 *matched = false;
                 return EOK;
             }
@@ -138,26 +170,32 @@ hbac_evaluate_time_rules(struct hbac_time_rules *rule,
 
     if(!rule->accesstimes[0]) {
         /* No match in exceptions and no accessTimes set -> the rule should apply */
+        free(cmp_time);
         *matched = true;
         return EOK;
     }
 
     if(rule->accesstimes) {
         for(i = 0; rule->accesstimes[i]; i++) {
-            ret = eval_time_rule(rule->accesstimes[i], cmp_time, matched);
+            ret = eval_time_rule(rule->accesstimes[i], cmp_time,
+                                 rule->timezone, matched);
             if(ret != EOK) {
+                free(cmp_time);
                 *matched = false;
                 return ENOMATCH;
             }
             else if(*matched == true) {
+                free(cmp_time);
                 return EOK;
             }
         }
     }
+
+    free(cmp_time);
     return ret;
 }
 
-struct tm *time_to_timezone(time_t t, const char *tz)
+struct tm *time_to_timezone(time_t t, const char *tz, struct tm **result)
 {
     struct tm *ret;
     char *env_backup = NULL;
@@ -182,11 +220,13 @@ struct tm *time_to_timezone(time_t t, const char *tz)
         return NULL;
 
     tzset();
-    ret = localtime(&t);
+    ret = localtime_r(&t, *result);
 
     if(env_backup != NULL)
         setenv("TZ", env_backup, 1);
     else unsetenv("TZ");
+
+    tzset();
     free(tz_toenv);
 
     return ret;
@@ -196,13 +236,20 @@ void get_token(const char *input, struct time_token *tok);
 errno_t check_range(int val, int low, int high);
 int get_week_of_month(struct tm *t);
 int get_week_of_year(struct tm *t);
+int parse_repeat(const char *str, struct repeat *rpt);
+errno_t check_repeat(struct repeat *rpt,
+                     struct tm *cmp_time,
+                     const char *tz,
+                     bool *found_flags);
 
 errno_t
 eval_time_rule(const char *rule,
-                 struct tm* cmp_time,
-                 bool *matched)
+               struct tm *cmp_time,
+               const char *tz,
+               bool *matched)
 {
     struct time_token tok;
+    struct repeat rpt;
     /*
      * array to store information about evaluation of each keyword
      *    false = Keyword not found (or found and is still being evaluated
@@ -211,18 +258,24 @@ eval_time_rule(const char *rule,
      */
     bool found_flags[START] = { false };
     enum time_types prev;
-    errno_t ret;
+    errno_t ret = EOK;
     int checked_val;
     char *rule_lower;
     int rule_it;
     int month_week;
     int year_week;
+    /* year modifier for special cases when "weekofyear" is part of the policy */
+    int year_mod = 0;
 
     prev = START;
     tok.type = START;
     tok.pos = 0;
     *matched = true;
 
+    rpt.type = NONE;
+    rpt.startdate = NULL;
+    rpt.enddate = NULL;
+
     if((rule_lower = malloc((strlen(rule)+1)*sizeof(char))) == NULL)
         return ENOMEM;
 
@@ -240,9 +293,9 @@ eval_time_rule(const char *rule,
          * year when the 1st January is Fri-Sun
          */
         if(year_week == 1 && cmp_time->tm_mon == 12)
-            cmp_time->tm_year++;
+            year_mod = 1;
         if(year_week > 51 && cmp_time->tm_mon == 1)
-            cmp_time->tm_year--;
+            year_mod = -1;
     }
 
     do {
@@ -251,7 +304,8 @@ eval_time_rule(const char *rule,
         if(tok.type < START) {
             /* Check only, don't set (for cases of multiple values at a keyword) */
             if(found_flags[tok.type]) {
-                return ENOMATCH;
+                ret = ENOMATCH;
+                goto finished;
             }
         }
 
@@ -357,11 +411,43 @@ eval_time_rule(const char *rule,
                     goto finished;
                 }
 
-                checked_val = cmp_time->tm_year;
+                checked_val = cmp_time->tm_year + year_mod;
 
                 break;
+            case RPT:
+                /* empty statement so that compiler does not throw error */
+                ;
+                int tmp_read = 0;
+                /* 11 is the shortest string length YYYYMMDD+xi */
+                if((tmp_read = parse_repeat(&rule_lower[tok.pos], &rpt)) < 11) {
+                    if(tmp_read == -1) {
+                        ret = EINVAL;
+                        goto finished;
+                    }
+                    if(tmp_read == -2) {
+                        ret = ENOMEM;
+                        goto finished;
+                    }
+                    ret = ENOMATCH;
+                    goto finished;
+                }
+                tok.pos += tmp_read;
+                /* it is necessary to set found_flags now, repeat only has one allowed value */
+                found_flags[RPT] = true;
+                checked_val = -1; /* so that the compiler does not throw warning */
+                break;
             case END:
                 ret = EOK;
+                /* if the last keyword range matched and RPT was found in the string*/
+                if(*matched && found_flags[RPT]) {
+                    /* the last keyword matched but its found_flags may not yet be set */
+                    found_flags[prev] = true;
+                    ret = check_repeat(&rpt, cmp_time, tz, found_flags);
+                    if(ret == ERANGE) {
+                        *matched = false;
+                        ret = EOK;
+                    }
+                }
                 goto finished;
             case MEM_ERR:
                 ret = ENOMEM;
@@ -371,7 +457,13 @@ eval_time_rule(const char *rule,
                 goto finished;
         }
 
-        ret = check_range(checked_val, tok.value_low, tok.value_high);
+        /*
+         * If RPT is found, it is evaluated in the end of the rule evaluation.
+         * The current state is that prev != tok.type, *matched is set to
+         * the last token evaluation match/nonmatch and ret is set to EOK.
+         */
+        if(tok.type != RPT)
+            ret = check_range(checked_val, tok.value_low, tok.value_high);
 
         /* invalid range */
         if (ret == EINVAL)
@@ -385,7 +477,6 @@ eval_time_rule(const char *rule,
         else {
             /* a new attribute */
             if(*matched == true) {
-                prev = tok.type;
                 *matched = (ret == EOK);
                 found_flags[prev] = true;
                 prev = tok.type;
@@ -401,18 +492,25 @@ eval_time_rule(const char *rule,
 
 finished:
     free(rule_lower);
+    if(rpt.startdate != NULL)
+        free(rpt.startdate);
+    if(rpt.enddate != NULL)
+        free(rpt.enddate);
     return ret;
 }
 
 int get_week_of_month(struct tm *t) {
-    /* ISO 8601 does not specify how to count a week in a month. It however
-     does specify how to count a week in a year. Such a week is at least
-     4 days long and starts with Monday */
+    /*
+     * ISO 8601 does not specify how to count a week in a month. It however
+     * does specify how to count a week in a year. Such a week is at least
+     * 4 days long and starts with Monday
+     */
 
     int sun; /* first sunday */
     int week = 0;
 
     /* mod 7 turns mday into 0-6 interval, -curr_day gets a sunday */
+    /* if curr_day == Sunday, (-7 + 7) addition happens */
     sun = (t->tm_mday % 7) - t->tm_wday;
     if(sun <= 0)
         sun +=7;
@@ -564,6 +662,14 @@ void get_token(const char *input, struct time_token *tok)
                 tok->type = YEAR;
                 idx += cmplen;
                 break;
+            case 'r':
+                cmplen = strlen(RPT_STR);
+                if(strncasecmp(&input[idx], RPT_STR, cmplen) != 0) {
+                    goto error;
+                }
+                tok->type = RPT;
+                idx += cmplen;
+                break;
             case '\0':
                 tok->type = END;
                 return;
@@ -589,6 +695,12 @@ void get_token(const char *input, struct time_token *tok)
     while(((c = input[idx]) != '\0') && isspace(c))
         idx++;
 
+    if(tok->type == RPT) {
+        /* parsing into the struct repeat happens elsewhere */
+        tok->pos = idx;
+        return;
+    }
+
     if((part = calloc(NUM_MAX_LEN, sizeof(char))) == NULL) {
         tok->type = MEM_ERR;
         return;
@@ -674,6 +786,149 @@ error:
     return;
 }
 
+bool validate_date(struct tm *t) {
+    /* 30-days months (mon_num-1 to match struct tm format) */
+    const int short_mon[] = {3, 5, 8, 10};
+    bool is_short = false;
+
+    for(int i = 0; i < 4; i++) {
+        if(t->tm_mon == short_mon[i]) {
+            is_short = true;
+            break;
+        }
+    }
+    if(is_short && (t->tm_mday > 30)) {
+        return false;
+    }
+    else if(t->tm_mon == 1) {
+        /* February */
+        if(isleap(t->tm_year + 1900)) {
+            if(t->tm_mday > 29)
+                return false;
+        }
+        else if(t->tm_mday > 28)
+            return false;
+    }
+
+    return true;
+}
+
+/*
+ * Parses the 'repeat' keyword string.
+ *
+ * On success, it returns the number of characters read. On error,
+ * it returns -1 for VAL_ERR and -2 for MEM_ERR
+ */
+int parse_repeat(const char *str, struct repeat *rpt) {
+    int read = 0;
+    /*
+     * The sane interval rate is probably a 3-digit number, but lets make max
+     * value 4-digit, just to make sure (+ \0)
+     */
+    char tmp_interval[5] = { 0 };
+    char *tmp;
+
+    /*
+     * check the correct format of the string - strptime does not require
+     * preceding zeros in month and day
+     */
+    while(read < 8) {
+        if(!isdigit(str[read++]))
+            return -1;
+    }
+
+    if((rpt->startdate = malloc(sizeof(struct tm))) == NULL)
+        return -2;
+
+    if((tmp = strptime(str, "%Y%m%d", rpt->startdate)) == NULL)
+        return -1;
+
+    rpt->startdate->tm_hour = 0;
+    rpt->startdate->tm_min = 0;
+    rpt->startdate->tm_sec = 0;
+    /* -1 in tm_isdst causes mktime not to change the values in the struct */
+    rpt->startdate->tm_isdst = -1;
+
+    if(!validate_date(rpt->startdate))
+        return -1;
+
+    if(str[read] == '-') {
+        read++;
+        /* date range */
+        /* 17 = first date(8)+ '-' + second date(8)*/
+        while(read < 17) {
+            if(!isdigit(str[read++]))
+                return -1;
+        }
+        if((rpt->enddate = malloc(sizeof(struct tm))) == NULL)
+            return -2;
+
+        if((tmp = strptime(&str[9], "%Y%m%d", rpt->enddate)) == NULL)
+            return -1;
+
+        rpt->enddate->tm_hour = 0;
+        rpt->enddate->tm_min = 0;
+        rpt->enddate->tm_sec = 0;
+        rpt->enddate->tm_isdst = -1;
+
+        if(!validate_date(rpt->enddate))
+            return -1;
+
+        if(rpt->enddate->tm_year < rpt->startdate->tm_year)
+            return -1;
+        else if(rpt->enddate->tm_year == rpt->startdate->tm_year) {
+            if(rpt->enddate->tm_mon < rpt->startdate->tm_mon)
+                return -1;
+            else if(rpt->enddate->tm_mon == rpt->startdate->tm_mon) {
+                if(rpt->enddate->tm_mday < rpt->startdate->tm_mday)
+                    return -1;
+            }
+        }
+    }
+
+    /* The INTERVAL part */
+    if(str[read++] != '+')
+        return -1;
+
+    for(int i = 0; i < 4; i++) {
+        if(!isdigit(tmp_interval[i] = str[read])) {
+            if(i == 0)
+                return -1;
+            else {
+                /* clear the last character for integer conversion */
+                tmp_interval[i] = 0;
+                break;
+            }
+        }
+        read++;
+    }
+
+    switch(str[read++]) {
+        case 'd':
+            rpt->type = DAILY;
+            break;
+        case 'w':
+            rpt->type = WEEKLY;
+            break;
+        case 'm':
+            rpt->type = MONTHLY;
+            break;
+        case 'y':
+            rpt->type = YEARLY;
+            break;
+        default:
+            return -1;
+    }
+
+    if(!isspace(str[read]) && str[read] != 0)
+        return -1;
+
+    /* atoi - no under/overflow or errors shoud occur */
+    rpt->interval = atoi(tmp_interval);
+
+    return read;
+}
+
 errno_t check_range(int val, int low, int high)
 {
     /* not a range */
@@ -699,3 +954,215 @@ errno_t check_range(int val, int low, int high)
         return ERANGE;
     return EOK;
 }
+
+errno_t
+get_repeat_timestamps(struct repeat *rpt,
+                          struct tm *cmp_time,
+                          const char *tz,
+                          time_t *start,
+                          time_t *end,
+                          time_t *cmp);
+/*
+ * Checks the range which is defined by the 'repeat' keyword.
+ * The 'repeat' keyword should simulate the behavior of recurrence rules as
+ * known from iCalendar BUT here the TIMEOFDAY (in iCalendar BYHOUR and BYMINUTE)
+ * LIMITS all the interval types (DAILY/WEEKLY/MONTHLY, YEARLY). If not set,
+ * the TIMEOFDAY here defaults to 0000-2359
+ */
+errno_t
+check_repeat(struct repeat *rpt,
+             struct tm *cmp_time,
+             const char *tz,
+             bool *found_flags)
+{
+    time_t start, end, now, diff;
+    struct tm *cmp_cpy;
+    int daydiff;
+    errno_t ret;
+
+    if((cmp_cpy = malloc(sizeof(struct tm))) == NULL)
+        return ENOMEM;
+
+    memcpy(cmp_cpy, cmp_time, sizeof(struct tm));
+    cmp_cpy->tm_hour = cmp_cpy->tm_min = cmp_cpy->tm_sec = 0;
+    /* conversion back to struct tm format, mktime only counts with mday, mon and year*/
+    cmp_cpy->tm_mon -= 1;
+    cmp_cpy->tm_year -= 1900;
+    /* If not set to null, both cmp_cpy and cmp_time __tm_zone pointers would point to a same place in memory */
+    cmp_cpy->__tm_zone = NULL;
+
+    if((ret = get_repeat_timestamps(rpt, cmp_cpy, tz, &start, &end, &now)) != EOK) {
+        free(cmp_cpy);
+        return ret;
+    }
+
+    /* timestamp does not include DST but in this case it should */
+    if(rpt->startdate->tm_isdst) start += 3600;
+    if(cmp_cpy->tm_isdst) now += 3600;
+
+    if(rpt->enddate != NULL) {
+        if(rpt->enddate->tm_isdst) end += 3600;
+
+        if(end < start) {
+            free(cmp_cpy);
+            return EINVAL;
+        }
+        if(now > end) {
+            free(cmp_cpy);
+            return ERANGE;
+        }
+    }
+
+    if(now < start) {
+        free(cmp_cpy);
+        return ERANGE;
+    }
+
+    diff = now - start;
+
+    /*
+     * 'repeat' is always evaluated last - no need to check whether TIMEOFDAY is
+     * set correctly, if set incorrectly, the evaluation would fail earlier
+     */
+    switch(rpt->type) {
+        case DAILY:
+            if((diff % (DAY_SECS * rpt->interval)) != 0) {
+                ret = ERANGE;
+                break;
+            }
+            ret = EOK;
+            break;
+        case WEEKLY:
+            if(!found_flags[DOW]) {
+                if((diff % (rpt->interval* DAY_SECS * 7)) != 0) {
+                    ret = ERANGE;
+                    break;
+                }
+            }
+            else {
+                /*
+                 * Recount the tstamp to the same day as in startdate to see
+                 * whether the day is correct for DAYOFWEEK settings. If
+                 * so, the rule should still apply, as the correct week is
+                 * encountered.
+                 */
+                daydiff = rpt->startdate->tm_wday - cmp_cpy->tm_wday;
+                diff += daydiff * DAY_SECS;
+                if((diff % (rpt->interval* DAY_SECS * 7)) != 0) {
+                    ret = ERANGE;
+                    break;
+                }
+            }
+            ret = EOK;
+            break;
+        case MONTHLY:
+            diff = (cmp_cpy->tm_year - rpt->startdate->tm_year - 1) * 12 +
+                    (cmp_cpy->tm_mon + 1) + (11 - rpt->startdate->tm_mon);
+
+            if((diff % rpt->interval) != 0) {
+                ret = ERANGE;
+                break;
+            }
+            else if(!found_flags[DOW] && !found_flags[DOM] && !found_flags[WOM]) {
+                /* if either of the above is set, it matched before this evaluation */
+                if(rpt->startdate->tm_mday != cmp_cpy->tm_mday) {
+                    ret = ERANGE;
+                    break;
+                }
+            }
+            ret = EOK;
+            break;
+        case YEARLY:
+            if(((cmp_cpy->tm_year - rpt->startdate->tm_year) % rpt->interval) != 0) {
+                ret = ERANGE;
+                break;
+            }
+            if(!found_flags[DOY] && !found_flags[WOY]) {
+                if(!found_flags[MOY]) {
+                    if(rpt->startdate->tm_mon != cmp_cpy->tm_mon) {
+                        ret = ERANGE;
+                        break;
+                    }
+                }
+                if(!found_flags[DOM] && !found_flags[DOW]) {
+                    if(rpt->startdate->tm_mday != cmp_cpy->tm_mday) {
+                        ret = ERANGE;
+                        break;
+                    }
+                }
+            }
+            ret = EOK;
+            break;
+        default:
+            ret = EINVAL;
+            break;
+    }
+
+    free(cmp_cpy);
+    return ret;
+
+}
+
+errno_t
+get_repeat_timestamps(struct repeat *rpt,
+                          struct tm *cmp_time,
+                          const char *tz,
+                          time_t *start,
+                          time_t *end,
+                          time_t *cmp)
+{
+    char *env_backup = NULL;
+    char *tz_toenv;
+    char *tmp = NULL;
+    int tzlen;
+
+    if(strcasecmp(tz, "host") == 0) {
+        *start = mktime(rpt->startdate);
+        if(rpt->enddate != NULL)
+            *end = mktime(rpt->enddate);
+        else *end = -1;
+        *cmp = mktime(cmp_time);
+        return EOK;
+    }
+
+    /* Timezone is either unset, set to UTC or set to a time zone name */
+    if(!tz) {
+        /* length of the 'utc' string */
+        tzlen = 3;
+        tz = strdup("utc");
+    }
+    else tzlen = strlen(tz);
+    /* + 2 because prepending string with ':'; end is a null sign */
+    tz_toenv = malloc((tzlen + 2) * sizeof(char));
+    if(tz_toenv == NULL)
+        return ENOMEM;
+    /* TZ variable is set */
+    if((tmp = getenv("TZ")) != NULL)
+        env_backup = strdup(tmp);
+
+    tz_toenv[0] = ':';
+    tz_toenv[tzlen+1] = '\0';
+    memcpy(&tz_toenv[1], tz, tzlen*sizeof(char));
+
+    if(setenv("TZ", tz_toenv, 1) != 0) {
+        free(tz_toenv);
+        return ENOMEM;
+    }
+
+    tzset();
+
+    *start = mktime(rpt->startdate);
+    if(rpt->enddate != NULL)
+        *end = mktime(rpt->enddate);
+    else *end = -1;
+    *cmp = mktime(cmp_time);
+
+    if(env_backup != NULL)
+        setenv("TZ", env_backup, 1);
+    else unsetenv("TZ");
+
+    tzset();
+    free(tz_toenv);
+
+    return EOK;
+}
-- 
2.4.3

