Daniel Reis
11 years ago
14 changed files with 540 additions and 0 deletions
-
22users_ldap_groups/__init__.py
-
62users_ldap_groups/__openerp__.py
-
2users_ldap_groups/security/ir.model.access.csv
-
98users_ldap_groups/users_ldap_groups.py
-
28users_ldap_groups/users_ldap_groups.xml
-
44users_ldap_groups/users_ldap_groups_operators.py
-
1users_ldap_populate/__init__.py
-
49users_ldap_populate/__openerp__.py
-
56users_ldap_populate/i18n/nl.po
-
2users_ldap_populate/model/__init__.py
-
42users_ldap_populate/model/populate_wizard.py
-
96users_ldap_populate/model/users_ldap.py
-
18users_ldap_populate/view/populate_wizard.xml
-
20users_ldap_populate/view/users_ldap.xml
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# OpenERP, Open Source Management Solution |
|||
# This module copyright (C) 2012 Therp BV (<http://therp.nl>). |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero 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 Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
import users_ldap_groups |
@ -0,0 +1,62 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# OpenERP, Open Source Management Solution |
|||
# This module copyright (C) 2012 Therp BV (<http://therp.nl>). |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero 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 Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
{ |
|||
"name" : "Groups assignment", |
|||
"version" : "1.0", |
|||
"depends" : ["auth_ldap"], |
|||
"author" : "Therp BV", |
|||
"description": """ |
|||
Adds user accounts to groups based on rules defined by the administrator. |
|||
|
|||
Usage: |
|||
|
|||
Define mappings in Settings->Companies->[your company]->tab configuration->[your |
|||
ldap server]. |
|||
|
|||
Decide whether you want only groups mapped from ldap (Only ldap groups=y) or a |
|||
mix of manually set groups and ldap groups (Only ldap groups=n). Setting this to |
|||
'no' will result in users never losing privileges when you remove them from a |
|||
ldap group, so that's a potential security issue. It is still the default to |
|||
prevent losing group information by accident. |
|||
|
|||
For active directory, use LDAP attribute 'memberOf' and operator 'contains'. |
|||
Fill in the DN of the windows group as value and choose an OpenERP group users |
|||
with this windows group are to be assigned to. |
|||
|
|||
For posix accounts, use operator 'query' and a value like |
|||
(&(cn=bzr)(objectClass=posixGroup)(memberUid=$uid)) |
|||
|
|||
The operator query matches if the filter in value returns something, and value |
|||
can contain $[attribute] which will be replaced by the first value of the |
|||
user's ldap record's attribute named [attribute]. |
|||
""", |
|||
"category" : "Tools", |
|||
"data" : [ |
|||
'users_ldap_groups.xml', |
|||
'security/ir.model.access.csv', |
|||
], |
|||
"installable": True, |
|||
"external_dependencies" : { |
|||
'python' : ['ldap'], |
|||
}, |
|||
} |
|||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
@ -0,0 +1,2 @@ |
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
|||
access_res_company_ldap_groups,res_company_ldap_groups,model_res_company_ldap_group_mapping,base.group_system,1,1,1,1 |
@ -0,0 +1,98 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# OpenERP, Open Source Management Solution |
|||
# This module copyright (C) 2012 Therp BV (<http://therp.nl>). |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero 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 Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from openerp.osv import fields, orm |
|||
import logging |
|||
import users_ldap_groups_operators |
|||
import inspect |
|||
import sys |
|||
|
|||
class CompanyLDAPGroupMapping(orm.Model): |
|||
_name='res.company.ldap.group_mapping' |
|||
_rec_name='ldap_attribute' |
|||
_order='ldap_attribute' |
|||
|
|||
def _get_operators(self, cr, uid, context=None): |
|||
operators=[] |
|||
for name, operator in inspect.getmembers(users_ldap_groups_operators, |
|||
lambda cls: inspect.isclass(cls) |
|||
and cls!=users_ldap_groups_operators.LDAPOperator): |
|||
operators.append((name, name)) |
|||
return tuple(operators) |
|||
|
|||
_columns={ |
|||
'ldap_id': fields.many2one('res.company.ldap', 'LDAP server', |
|||
required=True), |
|||
'ldap_attribute': fields.char('LDAP attribute', size=64, |
|||
help='The LDAP attribute to check.\n' |
|||
'For active directory, use memberOf.'), |
|||
'operator': fields.selection(_get_operators, 'Operator', |
|||
help='The operator to check the attribute against the value\n' |
|||
'For active directory, use \'contains\'', required=True), |
|||
'value': fields.char('Value', size=1024, |
|||
help='The value to check the attribute against.\n' |
|||
'For active directory, use the dn of the desired group', |
|||
required=True), |
|||
'group': fields.many2one('res.groups', 'OpenERP group', |
|||
help='The OpenERP group to assign', required=True), |
|||
} |
|||
|
|||
class CompanyLDAP(orm.Model): |
|||
_inherit='res.company.ldap' |
|||
|
|||
_columns={ |
|||
'group_mappings': fields.one2many('res.company.ldap.group_mapping', |
|||
'ldap_id', 'Group mappings', |
|||
help='Define how OpenERP groups are assigned to ldap users'), |
|||
'only_ldap_groups': fields.boolean('Only ldap groups', |
|||
help='If this is checked, manual changes to group membership are ' |
|||
'undone on every login (so OpenERP groups are always synchronous ' |
|||
'with LDAP groups). If not, manually added groups are preserved.') |
|||
} |
|||
|
|||
_default={ |
|||
'only_ldap_groups': False |
|||
} |
|||
|
|||
def get_or_create_user(self, cr, uid, conf, login, ldap_entry, context=None): |
|||
user_id=super(CompanyLDAP, self).get_or_create_user(cr, uid, conf, login, |
|||
ldap_entry, context) |
|||
if not user_id: |
|||
return user_id |
|||
logger=logging.getLogger('users_ldap_groups') |
|||
mappingobj=self.pool.get('res.company.ldap.group_mapping') |
|||
userobj=self.pool.get('res.users') |
|||
conf_all=self.read(cr, uid, conf['id'], ['only_ldap_groups']) |
|||
if(conf_all['only_ldap_groups']): |
|||
logger.debug('deleting all groups from user %d' % user_id) |
|||
userobj.write(cr, uid, user_id, {'groups_id': [(5, )]}) |
|||
|
|||
for mapping in mappingobj.read(cr, uid, mappingobj.search(cr, uid, |
|||
[('ldap_id', '=', conf['id'])]), []): |
|||
operator=getattr(users_ldap_groups_operators, mapping['operator'])() |
|||
logger.debug('checking mapping %s' % mapping) |
|||
if operator.check_value(ldap_entry, mapping['ldap_attribute'], |
|||
mapping['value'], conf, self, logger): |
|||
logger.debug('adding user %d to group %s' % |
|||
(user_id, mapping['group'][1])) |
|||
userobj.write(cr, uid, user_id, |
|||
{'groups_id': [(4, mapping['group'][0])]}) |
|||
return user_id |
@ -0,0 +1,28 @@ |
|||
<?xml version="1.0"?> |
|||
<openerp> |
|||
<data> |
|||
<record model="ir.ui.view" id="company_form_view"> |
|||
<field name="name">res.company.form.inherit.users_ldap_groups</field> |
|||
<field name="model">res.company</field> |
|||
<field name="type">form</field> |
|||
<field name="inherit_id" ref="auth_ldap.company_form_view"/> |
|||
<field name="arch" type="xml"> |
|||
|
|||
<xpath expr="//form[@string='LDAP Configuration']" position="inside"> |
|||
<group string="Map User Groups" > |
|||
<field name="only_ldap_groups" /> |
|||
<field name="group_mappings" colspan="4" nolabel="1"> |
|||
<tree editable="top"> |
|||
<field name="ldap_attribute" attrs="{'required': [('operator','not in',['query'])], 'readonly': [('operator','in',['query'])]}" /> |
|||
<field name="operator" /> |
|||
<field name="value" /> |
|||
<field name="group" /> |
|||
</tree> |
|||
</field> |
|||
</group> |
|||
</xpath> |
|||
|
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,44 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# OpenERP, Open Source Management Solution |
|||
# This module copyright (C) 2012 Therp BV (<http://therp.nl>). |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero 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 Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
from string import Template |
|||
|
|||
class LDAPOperator: |
|||
pass |
|||
|
|||
class contains(LDAPOperator): |
|||
def check_value(self, ldap_entry, attribute, value, ldap_config, company, |
|||
logger): |
|||
return (attribute in ldap_entry[1]) and (value in ldap_entry[1][attribute]) |
|||
|
|||
class equals(LDAPOperator): |
|||
def check_value(self, ldap_entry, attribute, value, ldap_config, company, |
|||
logger): |
|||
return (attribute in ldap_entry[1]) and (str(value)==str(ldap_entry[1][attribute])) |
|||
|
|||
class query(LDAPOperator): |
|||
def check_value(self, ldap_entry, attribute, value, ldap_config, company, |
|||
logger): |
|||
query_string=Template(value).safe_substitute(dict([(attribute, |
|||
ldap_entry[1][attribute][0]) for attribute in ldap_entry[1]])) |
|||
logger.debug('evaluating query group mapping, filter: %s'%query_string) |
|||
results=company.query(ldap_config, query_string) |
|||
logger.debug(results) |
|||
return bool(results) |
@ -0,0 +1 @@ |
|||
import model |
@ -0,0 +1,49 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# OpenERP, Open Source Management Solution |
|||
# This module copyright (C) 2012 Therp BV (<http://therp.nl>). |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero 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 Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
{ |
|||
"name": "LDAP Populate", |
|||
"version": "1.0", |
|||
"author": "Therp BV", |
|||
"category": 'Tools', |
|||
"description": """ |
|||
This module allows to prepopulate the user database with all entries in the LDAP |
|||
database. |
|||
|
|||
In order to schedule the population of the user database on a regular basis, |
|||
create a new scheduled action with the following properties: |
|||
|
|||
- Object: res.company.ldap |
|||
- Function: action_populate |
|||
- Arguments: [res.company.ldap.id] |
|||
|
|||
Substitute res.company.ldap.id with the actual id of the res.company.ldap object you want to query. |
|||
|
|||
""", |
|||
"depends": [ |
|||
'auth_ldap', |
|||
], |
|||
"data": [ |
|||
'view/users_ldap.xml', |
|||
'view/populate_wizard.xml', |
|||
], |
|||
'installable': True, |
|||
} |
|||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
@ -0,0 +1,56 @@ |
|||
# Translation of OpenERP Server. |
|||
# This file contains the translation of the following modules: |
|||
# * users_ldap_populate |
|||
# |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: OpenERP Server 6.1\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"POT-Creation-Date: 2012-07-31 13:08+0000\n" |
|||
"PO-Revision-Date: 2012-07-31 13:08+0000\n" |
|||
"Last-Translator: <>\n" |
|||
"Language-Team: \n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: \n" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: field:res.company.ldap.populate_wizard,ldap_id:0 |
|||
msgid "LDAP Configuration" |
|||
msgstr "LDAP configuratie" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: view:res.company:0 |
|||
msgid "Populate user database" |
|||
msgstr "Gebruikersbestand bevolken" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: field:res.company.ldap.populate_wizard,name:0 |
|||
msgid "Name" |
|||
msgstr "Naam" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: view:res.company:0 |
|||
msgid "Populate" |
|||
msgstr "Bevolken" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: model:ir.model,name:users_ldap_populate.model_res_company_ldap_populate_wizard |
|||
msgid "Populate users from LDAP" |
|||
msgstr "Gebruikersbestand bevolken op basis van LDAP" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: field:res.company.ldap.populate_wizard,users_created:0 |
|||
msgid "Number of users created" |
|||
msgstr "Aantal aangemaakte gebruikers" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: model:ir.model,name:users_ldap_populate.model_res_company_ldap |
|||
msgid "res.company.ldap" |
|||
msgstr "res.company.ldap" |
|||
|
|||
#. module: users_ldap_populate |
|||
#: view:res.company.ldap.populate_wizard:0 |
|||
msgid "OK" |
|||
msgstr "OK" |
@ -0,0 +1,2 @@ |
|||
import users_ldap |
|||
import populate_wizard |
@ -0,0 +1,42 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# OpenERP, Open Source Management Solution |
|||
# This module copyright (C) 2012 Therp BV (<http://therp.nl>). |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero 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 Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from osv import osv, fields |
|||
|
|||
class CompanyLDAPPopulateWizard(osv.TransientModel): |
|||
_name = 'res.company.ldap.populate_wizard' |
|||
_description = 'Populate users from LDAP' |
|||
_columns = { |
|||
'name': fields.char('Name', size=16), |
|||
'ldap_id': fields.many2one( |
|||
'res.company.ldap', 'LDAP Configuration'), |
|||
'users_created': fields.integer( |
|||
'Number of users created', readonly=True), |
|||
} |
|||
|
|||
def create(self, cr, uid, vals, context=None): |
|||
ldap_pool = self.pool.get('res.company.ldap') |
|||
users_pool = self.pool.get('res.users') |
|||
if 'ldap_id' in vals: |
|||
vals['users_created'] = ldap_pool.action_populate( |
|||
cr, uid, vals['ldap_id'], context=context) |
|||
return super(CompanyLDAPPopulateWizard, self).create( |
|||
cr, uid, vals, context=None) |
@ -0,0 +1,96 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# OpenERP, Open Source Management Solution |
|||
# This module copyright (C) 2012 Therp BV (<http://therp.nl>). |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Affero 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 Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
import re |
|||
from ldap.filter import filter_format |
|||
from openerp.osv import orm, fields |
|||
import openerp.exceptions |
|||
import logging |
|||
|
|||
class CompanyLDAP(orm.Model): |
|||
_inherit = 'res.company.ldap' |
|||
|
|||
def action_populate(self, cr, uid, ids, context=None): |
|||
""" |
|||
Prepopulate the user table from one or more LDAP resources. |
|||
|
|||
Obviously, the option to create users must be toggled in |
|||
the LDAP configuration. |
|||
|
|||
Return the number of users created (as far as we can tell). |
|||
""" |
|||
if isinstance(ids, (int, float)): |
|||
ids = [ids] |
|||
|
|||
users_pool = self.pool.get('res.users') |
|||
users_no_before = users_pool.search( |
|||
cr, uid, [], context=context, count=True) |
|||
logger = logging.getLogger('orm.ldap') |
|||
logger.debug("action_populate called on res.company.ldap ids %s", ids) |
|||
|
|||
for conf in self.get_ldap_dicts(cr, ids): |
|||
if not conf['create_user']: |
|||
continue |
|||
attribute_match = re.search( |
|||
'([a-zA-Z_]+)=\%s', conf['ldap_filter']) |
|||
if attribute_match: |
|||
login_attr = attribute_match.group(1) |
|||
else: |
|||
raise osv.except_osv( |
|||
"No login attribute found", |
|||
"Could not extract login attribute from filter %s" % |
|||
conf['ldap_filter']) |
|||
ldap_filter = filter_format(conf['ldap_filter'] % '*', ()) |
|||
for result in self.query(conf, ldap_filter): |
|||
self.get_or_create_user( |
|||
cr, uid, conf, result[1][login_attr][0], result) |
|||
|
|||
users_no_after = users_pool.search( |
|||
cr, uid, [], context=context, count=True) |
|||
users_created = users_no_after - users_no_before |
|||
logger.debug("%d users created", users_created) |
|||
return users_created |
|||
|
|||
def populate_wizard(self, cr, uid, ids, context=None): |
|||
""" |
|||
GUI wrapper for the populate method that reports back |
|||
the number of users created. |
|||
""" |
|||
if not ids: |
|||
return |
|||
if isinstance(ids, (int, float)): |
|||
ids = [ids] |
|||
wizard_obj = self.pool.get('res.company.ldap.populate_wizard') |
|||
res_id = wizard_obj.create( |
|||
cr, uid, {'ldap_id': ids[0]}, context=context) |
|||
|
|||
return { |
|||
'name': wizard_obj._description, |
|||
'view_type': 'form', |
|||
'view_mode': 'form', |
|||
'res_model': wizard_obj._name, |
|||
'domain': [], |
|||
'context': context, |
|||
'type': 'ir.actions.act_window', |
|||
'target': 'new', |
|||
'res_id': res_id, |
|||
'nodestroy': True, |
|||
} |
@ -0,0 +1,18 @@ |
|||
<?xml version="1.0"?> |
|||
<openerp> |
|||
<data> |
|||
<record model="ir.ui.view" id="populate_wizard_view"> |
|||
<field name="name">Add populate button to ldap view</field> |
|||
<field name="model">res.company.ldap.populate_wizard</field> |
|||
<field name="type">form</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Add populate button to ldap view" version="7.0"> |
|||
<group col="4"> |
|||
<field name="users_created"/> |
|||
<button icon="gtk-ok" string="OK" special="cancel"/> |
|||
</group> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
@ -0,0 +1,20 @@ |
|||
<?xml version="1.0"?> |
|||
<openerp> |
|||
<data> |
|||
<record model="ir.ui.view" id="company_form_view"> |
|||
<field name="name">Add populate button to ldap view</field> |
|||
<field name="model">res.company</field> |
|||
<field name="inherit_id" ref="auth_ldap.company_form_view"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//form[@string='LDAP Configuration']" position="inside"> |
|||
<separator string="Populate user database" colspan="4"/> |
|||
<button name="populate_wizard" |
|||
string="Populate" |
|||
type="object" |
|||
colspan="2"/> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
|||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue