예제 #1
0
def settings_oauth_token_exchange(actor, code):
    settings = system_settings.get_system_settings()

    log.debug(settings)
    url = "https://accounts.google.com/o/oauth2/token"
    payload = urllib.urlencode({
        "code":
        code,
        "client_id":
        settings["oauth2_google_client_id"],
        "client_secret":
        settings["oauth2_google_client_secret"],
        "redirect_uri":
        "http://%s/settings/oauth/token" % settings["host"],
        "grant_type":
        "authorization_code",
        "scope":
        ""
    })

    response = urlfetch.fetch(url, payload=payload, method=urlfetch.POST)
    if response.status_code >= 400:
        log.debug(response.status_code)
        log.debug(response.content)
        raise Exception()

    result = json.loads(response.content)
    system_settings.save_system_settings(actor, \
            { 'oauth2_google_request_token': str(result["refresh_token"]) })
예제 #2
0
def get_access_token_header(settings=None):
    settings = settings or system_settings.get_system_settings()
    if not "oauth2_google_client_id" in settings:
        raise Exception("OAuth2 Client ID not set")
    if not "oauth2_google_client_secret" in settings:
        raise Exception("OAuth2 Client Secret not set")
    if not "oauth2_google_request_token" in settings:
        raise Exception("OAuth2 Request Token not set")

    url = "https://accounts.google.com/o/oauth2/token"
    payload = urllib.urlencode({
        "client_id":
        settings["oauth2_google_client_id"],
        "client_secret":
        settings["oauth2_google_client_secret"],
        "grant_type":
        "refresh_token",
        "refresh_token":
        settings["oauth2_google_request_token"]
    })

    result = fetch(url, payload=payload, method=urlfetch.POST)

    token_type = str(result["token_type"])
    access_token = str(result["access_token"])

    log.debug("Access Token: %s %s" % (token_type, access_token))

    return {'Authorization': "%s %s" % (token_type, access_token)}
예제 #3
0
def error_report(viewer, url=None, request_code=None, description=None, stack=None, **ignored):
    settings = system_settings.get_system_settings()

    sender = viewer.user.email()
    if "email_admin" in settings:
        sender = settings["email_admin"]
    
    body = "%s\n\nURL: %s\nRequest: %s\n\n%s" % (description, url, request_code, stack)
    log.debug("Sending Email to Admins from %s:" % sender)
    log.debug(body)
    mail.send_mail_to_admins(sender, "Aegis Error - %s" % url, body)
예제 #4
0
def settings_oauth2_google_url(viewer):
    settings = system_settings.get_system_settings()
    scopes = [ "https://www.googleapis.com/auth/admin.directory.user.readonly",
               "https://www.googleapis.com/auth/admin.directory.group.readonly" ]

    return "https://accounts.google.com/o/oauth2/auth?%s" % urllib.urlencode( \
            { "client_id":       settings.get("oauth2_google_client_id"),
              "redirect_uri":    "http://%s/settings/oauth/token" % settings.get("host"),
              "scope":           " ".join(scopes),
              "response_type":   "code",
              "approval_prompt": "force",
              "access_type":     "offline" })
예제 #5
0
def settings_oauth2_google_url(viewer):
    settings = system_settings.get_system_settings()
    scopes = [
        "https://www.googleapis.com/auth/admin.directory.user.readonly",
        "https://www.googleapis.com/auth/admin.directory.group.readonly"
    ]

    return "https://accounts.google.com/o/oauth2/auth?%s" % urllib.urlencode( \
            { "client_id":       settings.get("oauth2_google_client_id"),
              "redirect_uri":    "http://%s/settings/oauth/token" % settings.get("host"),
              "scope":           " ".join(scopes),
              "response_type":   "code",
              "approval_prompt": "force",
              "access_type":     "offline" })
예제 #6
0
def error_report(viewer,
                 url=None,
                 request_code=None,
                 description=None,
                 stack=None,
                 **ignored):
    settings = system_settings.get_system_settings()

    sender = viewer.user.email()
    if "email_admin" in settings:
        sender = settings["email_admin"]

    body = "%s\n\nURL: %s\nRequest: %s\n\n%s" % (description, url,
                                                 request_code, stack)
    log.debug("Sending Email to Admins from %s:" % sender)
    log.debug(body)
    mail.send_mail_to_admins(sender, "Aegis Error - %s" % url, body)
예제 #7
0
def refresh_users(actor, force=False, **ignored):
    settings = system_settings.get_system_settings()
    if not force and settings.get("directory_sync", "disabled") != "enabled":
        log.debug("Directory action disabled")
        return

    if not "directory_domain" in settings:
        raise Exception("Directory domain not set")

    actor = actor or cron_user
    log.debug("Refreshing user list from directory: %s" % actor)

    auth_header = get_access_token_header(settings)

    user_count = 0
    alias_count = 0
    page_token = ""
    while (True):
        result = fetch("https://www.googleapis.com/admin/directory/v1/users?%s" % \
                    urllib.urlencode({ "domain":     settings["directory_domain"],
                                       "pageToken":  page_token,
                                       "maxResults": 500,
                                       "fields":     "nextPageToken,users/primaryEmail,users/name,users/emails" }), \
                    headers=auth_header)

        user_count += len(result["users"])
        for user in result["users"]:
            user_create_or_update(actor,
                                  user_id=user["primaryEmail"],
                                  first_name=user["name"]["givenName"],
                                  last_name=user["name"]["familyName"])

            alias_count += len(user["emails"])
            for alias in user["emails"]:
                alias_create(actor,
                             alias_id=alias["address"],
                             user_id=user["primaryEmail"])

        if not "nextPageToken" in result:
            break

        page_token = result["nextPageToken"]

    log.debug("Created or updated %s users, %s aliases" %
              (user_count, alias_count))
예제 #8
0
def refresh_users(actor, force=False, **ignored):
    settings = system_settings.get_system_settings()
    if not force and settings.get("directory_sync", "disabled") != "enabled":
        log.debug("Directory action disabled")
        return

    if not "directory_domain" in settings:
        raise Exception("Directory domain not set")

    actor = actor or cron_user
    log.debug("Refreshing user list from directory: %s" % actor)

    auth_header = get_access_token_header(settings)

    user_count = 0
    alias_count = 0
    page_token = ""
    while (True):
        result = fetch("https://www.googleapis.com/admin/directory/v1/users?%s" % \
                    urllib.urlencode({ "domain":     settings["directory_domain"],
                                       "pageToken":  page_token,
                                       "maxResults": 500,
                                       "fields":     "nextPageToken,users/primaryEmail,users/name,users/emails" }), \
                    headers=auth_header)

        user_count += len(result["users"])
        for user in result["users"]:
            user_create_or_update(actor, 
                                  user_id=user["primaryEmail"],
                                  first_name=user["name"]["givenName"],
                                  last_name=user["name"]["familyName"])

            alias_count += len(user["emails"])
            for alias in user["emails"]:
                alias_create(actor, alias_id=alias["address"], user_id=user["primaryEmail"])

        if not "nextPageToken" in result:
            break

        page_token = result["nextPageToken"]

    log.debug("Created or updated %s users, %s aliases" % (user_count, alias_count))
예제 #9
0
def settings_oauth_token_exchange(actor, code):
    settings = system_settings.get_system_settings()

    log.debug(settings)
    url = "https://accounts.google.com/o/oauth2/token"
    payload = urllib.urlencode({ "code":          code,
                                 "client_id":     settings["oauth2_google_client_id"],
                                 "client_secret": settings["oauth2_google_client_secret"],
                                 "redirect_uri":  "http://%s/settings/oauth/token" % settings["host"],
                                 "grant_type":    "authorization_code",
                                 "scope":         "" })

    response = urlfetch.fetch(url, payload=payload, method=urlfetch.POST)
    if response.status_code >= 400:
        log.debug(response.status_code)
        log.debug(response.content)
        raise Exception()

    result = json.loads(response.content)
    system_settings.save_system_settings(actor, \
            { 'oauth2_google_request_token': str(result["refresh_token"]) })
예제 #10
0
def get_access_token_header(settings=None):
    settings = settings or system_settings.get_system_settings()
    if not "oauth2_google_client_id" in settings:
        raise Exception("OAuth2 Client ID not set")
    if not "oauth2_google_client_secret" in settings:
        raise Exception("OAuth2 Client Secret not set")
    if not "oauth2_google_request_token" in settings:
        raise Exception("OAuth2 Request Token not set")

    url = "https://accounts.google.com/o/oauth2/token"
    payload = urllib.urlencode({ "client_id":     settings["oauth2_google_client_id"],
                                 "client_secret": settings["oauth2_google_client_secret"],
                                 "grant_type":    "refresh_token",
                                 "refresh_token": settings["oauth2_google_request_token"] })

    result = fetch(url, payload=payload, method=urlfetch.POST)

    token_type = str(result["token_type"])
    access_token = str(result["access_token"])

    log.debug("Access Token: %s %s" % (token_type, access_token))

    return { 'Authorization': "%s %s" % (token_type, access_token) }
예제 #11
0
def settings_load(viewer):
    return system_settings.get_system_settings()
예제 #12
0
def refresh_groups(actor, force=False, **ignored):
    settings = system_settings.get_system_settings()
    if not force and settings.get("directory_sync", "disabled") != "enabled":
        log.debug("Directory action disabled")
        return

    if not "directory_domain" in settings:
        raise Exception("Directory domain not set")

    actor = actor or cron_user
    log.debug("Refreshing group list from directory: %s" % actor)

    auth_header = get_access_token_header(settings)

    group_count = 0
    alias_count = 0
    member_count = 0
    page_token = ""
    all_groups = {}
    while (True):
        result = fetch("https://www.googleapis.com/admin/directory/v1/groups?%s" % \
                    urllib.urlencode({ "domain":     settings["directory_domain"],
                                       "pageToken":  page_token,
                                       "maxResults": 500,
                                       "fields":     ",".join([ "nextPageToken",
                                                                "groups/email",
                                                                "groups/name",
                                                                "groups/aliases",
                                                                "groups/id",
                                                                "groups/directMembersCount" ]) }), \
                    headers=auth_header)

        for group in result["groups"]:
            group_data = { 'email':   group["email"],
                           'name':    group["name"],
                           'aliases': [],
                           'members': [] }

            all_groups[group["email"]] = group_data

            if "aliases" in group:
                group_data["aliases"] = group["aliases"]

            log.debug("Getting members of group %s" % group["email"])
            if int(group["directMembersCount"]) > 0:
                member_page_token = ""
                while (True):
                    member_result = fetch("https://www.googleapis.com/admin/directory/v1/groups/%s/members?%s" % (group["id"], \
                                urllib.urlencode({ "pageToken": member_page_token,
                                                   "fields":    ",".join([ "nextPageToken",
                                                                           "members/email",
                                                                           "members/type" ]) })), \
                                headers=auth_header)

                    if "members" in member_result:
                        group_data["members"].extend(member_result["members"])

                    if not "nextPageToken" in member_result:
                        break

                    member_page_token = member_result["nextPageToken"]

        if not "nextPageToken" in result:
            break

        page_token = result["nextPageToken"]

    all_users = { user.user.email(): user for user in user_list_raw(actor) }

    def get_group_members(group_id, members):
        result = []
        for member in members:
            if member["type"] == "GROUP":
                if member["email"] in all_groups:
                    result.extend(get_group_members(group_id, all_groups[member["email"]]["members"]))
                else:
                    log.warn("Could not find group: %s" % member["email"])
            elif member["type"] == "USER":
                if member["email"] in all_users:
                    result.append(all_users[member["email"]])
            elif member["type"] == "CUSTOMER":
                result.extend(all_users.values())
            else:
                log.debug("%s" % member)
        return result

    group_count += len(all_groups)
    for group_name in sorted(all_groups):
        log.debug("Creating or updating group %s" % group_name)

        group = all_groups[group_name]
        group_create_or_update(actor, group_id=group["email"], name=group["name"], active=True)
        group_obj = build_group_key(group_name).get()
        alias_create(actor, alias_id=group["email"], group=group_obj)
        group_members_clear(actor, group=group_obj)

        alias_count += len(group["aliases"]) + 1
        for alias in group["aliases"]:
            alias_create(actor, alias_id=alias, group=group_obj)

        for member in get_group_members(group["email"], group["members"]):
            group_members_add(actor, group=group_obj, user=member)


    log.debug("Created or updated %s groups, %s aliases, %s members" % (group_count, alias_count, member_count))
예제 #13
0
def issue_update(actor,
                 issue_id=None,
                 key=None,
                 issue=None,
                 summary=undefined,
                 project=undefined,
                 status=undefined,
                 priority=undefined,
                 severity=undefined,
                 reporters=undefined,
                 assignees=undefined,
                 verifiers=undefined,
                 cc=undefined,
                 depends_on=undefined,
                 blocking=undefined,
                 privacy=undefined,
                 due_date=undefined,
                 body="",
                 send_mail=True,
                 blobs=undefined,
                 **args):
    issue = issue or (key or issue_key(issue_id)).get()
    header = ""

    is_root = permission_is_root(actor)

    if not is_root and issue.privacy != "public" and \
            build_user_key(actor) not in issue.cc + issue.assignees + issue.reporters + issue.verifiers:
        raise NotAllowedError()

    blob_list = None
    to_recipients = set([])
    if issue.assignees:
        to_recipients.update(issue.assignees)
    if issue.reporters:
        to_recipients.update(issue.reporters)
    if issue.verifiers:
        to_recipients.update(issue.verifiers)

    cc_recipients = set(issue.cc)

    if is_root or issue.privacy != "secure" or build_user_key(
            actor) in issue.assignees + issue.verifiers:
        # Rewrite input types if necessary
        def fix_users(field):
            log.debug(field)
            if is_undefined(field):
                return undefined
            if not field:
                return undefined
            if isinstance(field, basestring):
                return set([
                    build_user_key(id) for id in re.split("[\\s,;]+", field)
                    if len(id) > 0
                ])
            return [build_user_key(user) for user in field]

        reporters = fix_users(reporters)
        assignees = fix_users(assignees)
        verifiers = fix_users(verifiers)
        cc = fix_users(cc)

        if is_defined(depends_on):
            depends_on = set([
                issue_key(id) for id in re.split("[\\s,;]+", depends_on)
                if len(id) > 0
            ])

        if is_defined(blocking):
            blocking = set([
                issue_key(id) for id in re.split("[\\s,;]+", blocking)
                if len(id) > 0
            ])

        if is_defined(due_date):
            if due_date == "":
                due_date = None
            else:
                if "timezoneoffset" in args:
                    offset = datetime.timedelta(
                        minutes=int(args["timezoneoffset"]))
                    client_time = datetime.datetime.utcnow() - offset
                    parsed_time = lib.parsedatetime.Calendar().parse(
                        due_date, client_time)
                    log.debug("client_time = %s" % client_time)
                else:
                    offset = datetime.timedelta(0)
                    parsed_time = lib.parsedatetime.Calendar().parse(due_date)

                if parsed_time[1] == 1:
                    due_date = datetime.datetime(*parsed_time[0][:3]) + offset
                else:
                    due_date = datetime.datetime(*parsed_time[0][:6]) + offset

        # Update all fields
        if is_defined(summary) and summary != issue.summary:
            header = header + "**Summary:** " + summary + "  \n"
            issue.summary = summary
            issue.summary_index = set(re.split("[^\\w\\d]+", summary.lower()))

        if is_defined(project) and project != issue.project:
            header = header + "**Project:** " + project + "  \n"
            issue.project = project

        if is_defined(status) and status != issue.status:
            if not status in issue_transitions:
                raise IllegalError("Status not recognized")
            if not status in issue_transitions[issue.status]:
                raise IllegalError("Status transition not allowed")
            header = header + "**Status:** " + status + "  \n"
            issue.status = status

        if is_defined(priority) and int(priority) != issue.priority:
            header = header + "**Priority:** " + str(priority) + "  \n"
            issue.priority = int(priority)

        if is_defined(severity) and int(severity) != issue.severity:
            header = header + "**Severity:** " + str(severity) + "  \n"
            issue.severity = int(severity)

        if is_defined(reporters) and reporters != set(issue.reporters):
            log.debug("Reporters: %s" % reporters)
            header = header + "**Reporters:** " + ", ".join(
                [user.id() for user in reporters]) + "  \n"
            issue.reporters = reporters

        if is_defined(assignees) and assignees != set(issue.assignees):
            header = header + "**Assignees:** " + ", ".join(
                [user.id() for user in assignees]) + "  \n"
            issue.assignees = assignees

        if is_defined(verifiers) and verifiers != set(issue.verifiers):
            header = header + "**Verifiers:** " + ", ".join(
                [user.id() for user in verifiers]) + "  \n"
            issue.verifiers = verifiers

        if is_defined(cc) and cc != set(issue.cc):
            header = header + "**CC:** " + ", ".join(
                [user.id() for user in cc]) + "  \n"
            issue.cc = list(cc)

        if is_defined(depends_on) and depends_on != set(issue.depends_on):
            header = header + "**Depends On:** " + ", ".join(
                [str(iss.id()) for iss in depends_on]) + "  \n"
            issue.depends_on = list(depends_on)

        if is_defined(blocking) and blocking != set(issue.blocking):
            header = header + "**Blocking:** " + ", ".join(
                [str(iss.id()) for iss in blocking]) + "  \n"
            issue.blocking = list(blocking)

        if is_defined(privacy) and privacy != issue.privacy:
            header = header + "**Privacy:** " + privacy + "  \n"
            issue.privacy = privacy

        if is_defined(due_date) and due_date != issue.due_date:
            header = header + "**Due Date:** " + str(due_date) + " UTC  \n"
            issue.due_date = due_date

        if is_defined(blobs) and blobs:
            blob_list = build_blob_keys(blobs)
            header = header + "**Attachments:** %s File(s)  \n" % len(
                blob_list)

        # Fix up missing stuff

        if not issue.reporters: issue.reporters = [build_user_key(actor)]
        if not issue.verifiers: issue.verifiers = [build_user_key(actor)]
        if not issue.assignees: issue.assignees = [build_user_key(actor)]

        to_recipients.update(issue.assignees)
        to_recipients.update(issue.reporters)
        to_recipients.update(issue.verifiers)
        cc_recipients.update(issue.cc)

    issue.score, issue.score_description = calculate_issue_score(issue)

    issue.text_index = set(issue.text_index) | \
                       set(issue.summary_index) | \
                       set(re.split("[^\\w\\d]+", body.lower()))

    issue.updated_by = build_user_key(actor)
    issue.put()

    issue.history = [
        remark_create(actor,
                      issue.key,
                      body.strip(),
                      header.strip(),
                      blobs=blob_list)
    ]

    if send_mail:
        settings = get_system_settings()
        if "host" in settings and settings["host"] and settings["host"] != "":
            host = settings["host"]
        else:
            host = "%s.appspot.com" % app_identity.get_application_id()
        message_id = "<issue-%s@%s>" % (issue.key.id(),
                                        app_identity.get_application_id())
        url = "http://%s/issues/%s" % (host, issue.key.id())
        text = "%s\n\n%s\n\n%s" % (header, body, url)
        html = "<div style='font-size: 0.8em'>%s</div><div>%s</div><div>%s</div>" % \
                    (lib.markdown.markdown(header), lib.markdown.markdown(body), url)
        try:
            if len(cc_recipients) > 0:
                mail.send_mail(sender=actor.user.email(),
                               to=[user.id() for user in to_recipients],
                               cc=[user.id() for user in cc_recipients],
                               reply_to=actor.user.email(),
                               subject="[" + str(issue.key.id()) + "] " +
                               issue.summary,
                               body=text,
                               html=html,
                               headers={
                                   "In-Reply-To": message_id,
                                   "References": message_id
                               })
            else:
                mail.send_mail(sender=actor.user.email(),
                               to=[user.id() for user in to_recipients],
                               reply_to=actor.user.email(),
                               subject="[" + str(issue.key.id()) + "] " +
                               issue.summary,
                               body=text,
                               html=html,
                               headers={
                                   "In-Reply-To": message_id,
                                   "References": message_id
                               })
            log.debug("Email sent to %s" % (to_recipients | cc_recipients))
        except:
            log.warn("Email quota exceeded, email not sent")
        log.debug(text)

    return to_model(actor, issue)
예제 #14
0
def settings_load(viewer):
    return system_settings.get_system_settings()
예제 #15
0
def refresh_groups(actor, force=False, **ignored):
    settings = system_settings.get_system_settings()
    if not force and settings.get("directory_sync", "disabled") != "enabled":
        log.debug("Directory action disabled")
        return

    if not "directory_domain" in settings:
        raise Exception("Directory domain not set")

    actor = actor or cron_user
    log.debug("Refreshing group list from directory: %s" % actor)

    auth_header = get_access_token_header(settings)

    group_count = 0
    alias_count = 0
    member_count = 0
    page_token = ""
    all_groups = {}
    while (True):
        result = fetch("https://www.googleapis.com/admin/directory/v1/groups?%s" % \
                    urllib.urlencode({ "domain":     settings["directory_domain"],
                                       "pageToken":  page_token,
                                       "maxResults": 500,
                                       "fields":     ",".join([ "nextPageToken",
                                                                "groups/email",
                                                                "groups/name",
                                                                "groups/aliases",
                                                                "groups/id",
                                                                "groups/directMembersCount" ]) }), \
                    headers=auth_header)

        for group in result["groups"]:
            group_data = {
                'email': group["email"],
                'name': group["name"],
                'aliases': [],
                'members': []
            }

            all_groups[group["email"]] = group_data

            if "aliases" in group:
                group_data["aliases"] = group["aliases"]

            log.debug("Getting members of group %s" % group["email"])
            if int(group["directMembersCount"]) > 0:
                member_page_token = ""
                while (True):
                    member_result = fetch("https://www.googleapis.com/admin/directory/v1/groups/%s/members?%s" % (group["id"], \
                                urllib.urlencode({ "pageToken": member_page_token,
                                                   "fields":    ",".join([ "nextPageToken",
                                                                           "members/email",
                                                                           "members/type" ]) })), \
                                headers=auth_header)

                    if "members" in member_result:
                        group_data["members"].extend(member_result["members"])

                    if not "nextPageToken" in member_result:
                        break

                    member_page_token = member_result["nextPageToken"]

        if not "nextPageToken" in result:
            break

        page_token = result["nextPageToken"]

    all_users = {user.user.email(): user for user in user_list_raw(actor)}

    def get_group_members(group_id, members):
        result = []
        for member in members:
            if member["type"] == "GROUP":
                if member["email"] in all_groups:
                    result.extend(
                        get_group_members(
                            group_id, all_groups[member["email"]]["members"]))
                else:
                    log.warn("Could not find group: %s" % member["email"])
            elif member["type"] == "USER":
                if member["email"] in all_users:
                    result.append(all_users[member["email"]])
            elif member["type"] == "CUSTOMER":
                result.extend(all_users.values())
            else:
                log.debug("%s" % member)
        return result

    group_count += len(all_groups)
    for group_name in sorted(all_groups):
        log.debug("Creating or updating group %s" % group_name)

        group = all_groups[group_name]
        group_create_or_update(actor,
                               group_id=group["email"],
                               name=group["name"],
                               active=True)
        group_obj = build_group_key(group_name).get()
        alias_create(actor, alias_id=group["email"], group=group_obj)
        group_members_clear(actor, group=group_obj)

        alias_count += len(group["aliases"]) + 1
        for alias in group["aliases"]:
            alias_create(actor, alias_id=alias, group=group_obj)

        for member in get_group_members(group["email"], group["members"]):
            group_members_add(actor, group=group_obj, user=member)

    log.debug("Created or updated %s groups, %s aliases, %s members" %
              (group_count, alias_count, member_count))
예제 #16
0
def issue_update(actor, issue_id=None, key=None, issue=None, summary=undefined, project=undefined, 
                 status=undefined, priority=undefined, severity=undefined, reporters=undefined, 
                 assignees=undefined, verifiers=undefined, cc=undefined, depends_on=undefined, 
                 blocking=undefined, privacy=undefined, due_date=undefined, body="", send_mail=True, 
                 blobs=undefined, **args):
    issue = issue or (key or issue_key(issue_id)).get()
    header = ""

    is_root = permission_is_root(actor)

    if not is_root and issue.privacy != "public" and \
            build_user_key(actor) not in issue.cc + issue.assignees + issue.reporters + issue.verifiers:
        raise NotAllowedError()

    blob_list = None
    to_recipients = set([])
    if issue.assignees:
        to_recipients.update(issue.assignees)
    if issue.reporters:
        to_recipients.update(issue.reporters)
    if issue.verifiers:
        to_recipients.update(issue.verifiers)

    cc_recipients = set(issue.cc)

    if is_root or issue.privacy != "secure" or build_user_key(actor) in issue.assignees + issue.verifiers:
        # Rewrite input types if necessary
        def fix_users(field):
            log.debug(field)
            if is_undefined(field):
                return undefined
            if not field:
                return undefined
            if isinstance(field, basestring):
                return set([build_user_key(id) for id in re.split("[\\s,;]+", field) if len(id) > 0])
            return [build_user_key(user) for user in field]
    
        reporters = fix_users(reporters)
        assignees = fix_users(assignees)
        verifiers = fix_users(verifiers)
        cc = fix_users(cc)

        if is_defined(depends_on):
            depends_on = set([issue_key(id) for id in re.split("[\\s,;]+", depends_on) if len(id) > 0])
    
        if is_defined(blocking):
            blocking = set([issue_key(id) for id in re.split("[\\s,;]+", blocking) if len(id) > 0])

        if is_defined(due_date):
            if due_date == "":
                due_date = None
            else:
                if "timezoneoffset" in args:
                    offset = datetime.timedelta(minutes=int(args["timezoneoffset"]))
                    client_time = datetime.datetime.utcnow() - offset
                    parsed_time = lib.parsedatetime.Calendar().parse(due_date, client_time)
                    log.debug("client_time = %s" % client_time)
                else:
                    offset = datetime.timedelta(0)
                    parsed_time = lib.parsedatetime.Calendar().parse(due_date)

                if parsed_time[1] == 1:
                    due_date = datetime.datetime(*parsed_time[0][:3]) + offset
                else:
                    due_date = datetime.datetime(*parsed_time[0][:6]) + offset

        # Update all fields
        if is_defined(summary) and summary != issue.summary:
            header = header + "**Summary:** " + summary + "  \n"
            issue.summary = summary
            issue.summary_index = set(re.split("[^\\w\\d]+", summary.lower()))
    
        if is_defined(project) and project != issue.project:
            header = header + "**Project:** " + project + "  \n"
            issue.project = project

        if is_defined(status) and status != issue.status:
            if not status in issue_transitions:
                raise IllegalError("Status not recognized")
            if not status in issue_transitions[issue.status]:
                raise IllegalError("Status transition not allowed")
            header = header + "**Status:** " + status + "  \n"
            issue.status = status
    
        if is_defined(priority) and int(priority) != issue.priority:
            header = header + "**Priority:** " + str(priority) + "  \n"
            issue.priority = int(priority)
    
        if is_defined(severity) and int(severity) != issue.severity:
            header = header + "**Severity:** " + str(severity) + "  \n"
            issue.severity = int(severity)
    
        if is_defined(reporters) and reporters != set(issue.reporters):
            log.debug("Reporters: %s" % reporters)
            header = header + "**Reporters:** " + ", ".join([user.id() for user in reporters]) + "  \n"
            issue.reporters = reporters
    
        if is_defined(assignees) and assignees != set(issue.assignees):
            header = header + "**Assignees:** " + ", ".join([user.id() for user in assignees]) + "  \n"
            issue.assignees = assignees
    
        if is_defined(verifiers) and verifiers != set(issue.verifiers):
            header = header + "**Verifiers:** " + ", ".join([user.id() for user in verifiers]) + "  \n"
            issue.verifiers = verifiers
    
        if is_defined(cc) and cc != set(issue.cc):
            header = header + "**CC:** " + ", ".join([user.id() for user in cc]) + "  \n"
            issue.cc = list(cc)
    
        if is_defined(depends_on) and depends_on != set(issue.depends_on):
            header = header + "**Depends On:** " + ", ".join([str(iss.id()) for iss in depends_on]) + "  \n"
            issue.depends_on = list(depends_on)
    
        if is_defined(blocking) and blocking != set(issue.blocking):
            header = header + "**Blocking:** " + ", ".join([str(iss.id()) for iss in blocking]) + "  \n"
            issue.blocking = list(blocking)
    
        if is_defined(privacy) and privacy != issue.privacy:
            header = header + "**Privacy:** " + privacy + "  \n"
            issue.privacy = privacy

        if is_defined(due_date) and due_date != issue.due_date:
            header = header + "**Due Date:** " + str(due_date) + " UTC  \n"
            issue.due_date = due_date

        if is_defined(blobs) and blobs:
            blob_list = build_blob_keys(blobs)
            header = header + "**Attachments:** %s File(s)  \n" % len(blob_list)

        # Fix up missing stuff

        if not issue.reporters: issue.reporters = [ build_user_key(actor) ]
        if not issue.verifiers: issue.verifiers = [ build_user_key(actor) ]
        if not issue.assignees: issue.assignees = [ build_user_key(actor) ]

        to_recipients.update(issue.assignees)
        to_recipients.update(issue.reporters)
        to_recipients.update(issue.verifiers)
        cc_recipients.update(issue.cc)

    issue.score, issue.score_description = calculate_issue_score(issue)

    issue.text_index = set(issue.text_index) | \
                       set(issue.summary_index) | \
                       set(re.split("[^\\w\\d]+", body.lower()))

    issue.updated_by = build_user_key(actor)
    issue.put()

    issue.history = [ remark_create(actor, issue.key, body.strip(), header.strip(), blobs=blob_list) ]

    if send_mail:
        settings = get_system_settings()
        if "host" in settings and settings["host"] and settings["host"] != "":
            host = settings["host"]
        else:
            host = "%s.appspot.com" % app_identity.get_application_id()
        message_id = "<issue-%s@%s>" % (issue.key.id(), app_identity.get_application_id())
        url = "http://%s/issues/%s" % (host, issue.key.id())
        text = "%s\n\n%s\n\n%s" % (header, body, url)
        html = "<div style='font-size: 0.8em'>%s</div><div>%s</div><div>%s</div>" % \
                    (lib.markdown.markdown(header), lib.markdown.markdown(body), url)
        try:
            if len(cc_recipients) > 0:
                mail.send_mail(sender=actor.user.email(),
                               to=[user.id() for user in to_recipients],
                               cc=[user.id() for user in cc_recipients],
                               reply_to=actor.user.email(),
                               subject="[" + str(issue.key.id()) + "] " + issue.summary,
                               body=text,
                               html=html,
                               headers={"In-Reply-To": message_id, "References":  message_id })
            else:
                mail.send_mail(sender=actor.user.email(),
                               to=[user.id() for user in to_recipients],
                               reply_to=actor.user.email(),
                               subject="[" + str(issue.key.id()) + "] " + issue.summary,
                               body=text,
                               html=html,
                               headers={"In-Reply-To": message_id, "References":  message_id })
            log.debug("Email sent to %s" % (to_recipients | cc_recipients))
        except:
            log.warn("Email quota exceeded, email not sent")
        log.debug(text)

    return to_model(actor, issue)