mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-23 17:12:09 +02:00
Initial commit: Security packages
This commit is contained in:
commit
bb469e4763
1399 changed files with 278378 additions and 0 deletions
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import google_calendar
|
||||
from . import google_event
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from uuid import uuid4
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.google_calendar.utils.google_event import GoogleEvent
|
||||
from odoo.addons.google_account.models.google_service import TIMEOUT
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def requires_auth_token(func):
|
||||
def wrapped(self, *args, **kwargs):
|
||||
if not kwargs.get('token'):
|
||||
raise AttributeError("An authentication token is required")
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
class InvalidSyncToken(Exception):
|
||||
pass
|
||||
|
||||
class GoogleCalendarService():
|
||||
|
||||
def __init__(self, google_service):
|
||||
self.google_service = google_service
|
||||
|
||||
@requires_auth_token
|
||||
def get_events(self, sync_token=None, token=None, timeout=TIMEOUT):
|
||||
url = "/calendar/v3/calendars/primary/events"
|
||||
headers = {'Content-type': 'application/json'}
|
||||
params = {'access_token': token}
|
||||
if sync_token:
|
||||
params['syncToken'] = sync_token
|
||||
else:
|
||||
# full sync, limit to a range of 1y in past to 1y in the futur by default
|
||||
ICP = self.google_service.env['ir.config_parameter'].sudo()
|
||||
day_range = int(ICP.get_param('google_calendar.sync.range_days', default=365))
|
||||
_logger.info("Full cal sync, restricting to %s days range", day_range)
|
||||
lower_bound = fields.Datetime.subtract(fields.Datetime.now(), days=day_range)
|
||||
upper_bound = fields.Datetime.add(fields.Datetime.now(), days=day_range)
|
||||
params['timeMin'] = lower_bound.isoformat() + 'Z' # Z = UTC (RFC3339)
|
||||
params['timeMax'] = upper_bound.isoformat() + 'Z' # Z = UTC (RFC3339)
|
||||
try:
|
||||
status, data, time = self.google_service._do_request(url, params, headers, method='GET', timeout=timeout)
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code == 410 and 'fullSyncRequired' in str(e.response.content):
|
||||
raise InvalidSyncToken("Invalid sync token. Full sync required")
|
||||
raise e
|
||||
|
||||
events = data.get('items', [])
|
||||
next_page_token = data.get('nextPageToken')
|
||||
while next_page_token:
|
||||
params = {'access_token': token, 'pageToken': next_page_token}
|
||||
status, data, time = self.google_service._do_request(url, params, headers, method='GET', timeout=timeout)
|
||||
next_page_token = data.get('nextPageToken')
|
||||
events += data.get('items', [])
|
||||
|
||||
next_sync_token = data.get('nextSyncToken')
|
||||
default_reminders = data.get('defaultReminders')
|
||||
|
||||
return GoogleEvent(events), next_sync_token, default_reminders
|
||||
|
||||
@requires_auth_token
|
||||
def insert(self, values, token=None, timeout=TIMEOUT):
|
||||
send_updates = self.google_service._context.get('send_updates', True)
|
||||
url = "/calendar/v3/calendars/primary/events?conferenceDataVersion=1&sendUpdates=%s" % ("all" if send_updates else "none")
|
||||
headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
|
||||
if not values.get('id'):
|
||||
values['id'] = uuid4().hex
|
||||
self.google_service._do_request(url, json.dumps(values), headers, method='POST', timeout=timeout)
|
||||
return values['id']
|
||||
|
||||
@requires_auth_token
|
||||
def patch(self, event_id, values, token=None, timeout=TIMEOUT):
|
||||
url = "/calendar/v3/calendars/primary/events/%s?sendUpdates=all" % event_id
|
||||
headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
|
||||
self.google_service._do_request(url, json.dumps(values), headers, method='PATCH', timeout=timeout)
|
||||
|
||||
@requires_auth_token
|
||||
def delete(self, event_id, token=None, timeout=TIMEOUT):
|
||||
url = "/calendar/v3/calendars/primary/events/%s?sendUpdates=all" % event_id
|
||||
headers = {'Content-type': 'application/json'}
|
||||
params = {'access_token': token}
|
||||
# Delete all events from recurrence in a single request to Google and triggering a single mail.
|
||||
# The 'singleEvents' parameter is a trick that tells Google API to delete all recurrent events individually,
|
||||
# making the deletion be handled entirely on their side, and then we archive the events in Odoo.
|
||||
is_recurrence = self.google_service._context.get('is_recurrence', True)
|
||||
if is_recurrence:
|
||||
params['singleEvents'] = 'true'
|
||||
try:
|
||||
self.google_service._do_request(url, params, headers=headers, method='DELETE', timeout=timeout)
|
||||
except requests.HTTPError as e:
|
||||
# For some unknown reason Google can also return a 403 response when the event is already cancelled.
|
||||
if e.response.status_code not in (410, 403):
|
||||
raise e
|
||||
_logger.info("Google event %s was already deleted" % event_id)
|
||||
|
||||
|
||||
#################################
|
||||
## MANAGE CONNEXION TO GMAIL ##
|
||||
#################################
|
||||
|
||||
|
||||
def is_authorized(self, user):
|
||||
return bool(user.sudo().google_calendar_rtoken)
|
||||
|
||||
def _get_calendar_scope(self, RO=False):
|
||||
readonly = '.readonly' if RO else ''
|
||||
return 'https://www.googleapis.com/auth/calendar%s' % (readonly)
|
||||
|
||||
def _google_authentication_url(self, from_url='http://www.odoo.com'):
|
||||
state = {
|
||||
'd': self.google_service.env.cr.dbname,
|
||||
's': 'calendar',
|
||||
'f': from_url
|
||||
}
|
||||
base_url = self.google_service._context.get('base_url') or self.google_service.get_base_url()
|
||||
return self.google_service._get_authorize_uri(
|
||||
'calendar',
|
||||
self._get_calendar_scope(),
|
||||
base_url + '/google_account/authentication',
|
||||
state=json.dumps(state),
|
||||
approval_prompt='force',
|
||||
access_type='offline'
|
||||
)
|
||||
|
||||
def _can_authorize_google(self, user):
|
||||
return user.has_group('base.group_erp_manager')
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import json
|
||||
|
||||
from odoo.tools import email_normalize, ReadonlyDict
|
||||
import logging
|
||||
from typing import Iterator, Mapping
|
||||
from collections import abc
|
||||
import re
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GoogleEvent(abc.Set):
|
||||
"""This helper class holds the values of a Google event.
|
||||
Inspired by Odoo recordset, one instance can be a single Google event or a
|
||||
(immutable) set of Google events.
|
||||
All usual set operations are supported (union, intersection, etc).
|
||||
|
||||
A list of all attributes can be found in the API documentation.
|
||||
https://developers.google.com/calendar/v3/reference/events#resource
|
||||
|
||||
:param iterable: iterable of GoogleCalendar instances or iterable of dictionnaries
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, iterable=()):
|
||||
_events = {}
|
||||
for item in iterable:
|
||||
if isinstance(item, self.__class__):
|
||||
_events[item.id] = item._events[item.id]
|
||||
elif isinstance(item, Mapping):
|
||||
_events[item.get('id')] = item
|
||||
else:
|
||||
raise ValueError("Only %s or iterable of dict are supported" % self.__class__.__name__)
|
||||
self._events = ReadonlyDict(_events)
|
||||
|
||||
def __iter__(self) -> Iterator['GoogleEvent']:
|
||||
return iter(GoogleEvent([vals]) for vals in self._events.values())
|
||||
|
||||
def __contains__(self, google_event):
|
||||
return google_event.id in self._events
|
||||
|
||||
def __len__(self):
|
||||
return len(self._events)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._events)
|
||||
|
||||
def __getattr__(self, name):
|
||||
# ensure_one
|
||||
try:
|
||||
event, = self._events.keys()
|
||||
except ValueError:
|
||||
raise ValueError("Expected singleton: %s" % self)
|
||||
event_id = list(self._events.keys())[0]
|
||||
value = self._events[event_id].get(name)
|
||||
json.dumps(value)
|
||||
return value
|
||||
|
||||
def __repr__(self):
|
||||
return '%s%s' % (self.__class__.__name__, self.ids)
|
||||
|
||||
@property
|
||||
def ids(self):
|
||||
return tuple(e.id for e in self)
|
||||
|
||||
@property
|
||||
def rrule(self):
|
||||
if self.recurrence and any('RRULE' in item for item in self.recurrence):
|
||||
return next(item for item in self.recurrence if 'RRULE' in item)
|
||||
|
||||
def odoo_id(self, env):
|
||||
self.odoo_ids(env) # load ids
|
||||
return self._odoo_id
|
||||
|
||||
def _meta_odoo_id(self, dbname):
|
||||
"""Returns the Odoo id stored in the Google Event metadata.
|
||||
This id might not actually exists in the database.
|
||||
"""
|
||||
properties = self.extendedProperties and (self.extendedProperties.get('shared', {}) or self.extendedProperties.get('private', {})) or {}
|
||||
o_id = properties.get('%s_odoo_id' % dbname)
|
||||
if o_id:
|
||||
return int(o_id)
|
||||
|
||||
def odoo_ids(self, env):
|
||||
ids = tuple(e._odoo_id for e in self if e._odoo_id)
|
||||
if len(ids) == len(self):
|
||||
return ids
|
||||
model = self._get_model(env)
|
||||
found = self._load_odoo_ids_from_db(env, model)
|
||||
unsure = self - found
|
||||
if unsure:
|
||||
unsure._load_odoo_ids_from_metadata(env, model)
|
||||
|
||||
return tuple(e._odoo_id for e in self)
|
||||
|
||||
def _load_odoo_ids_from_metadata(self, env, model):
|
||||
unsure_odoo_ids = tuple(e._meta_odoo_id(env.cr.dbname) for e in self)
|
||||
odoo_events = model.browse(_id for _id in unsure_odoo_ids if _id)
|
||||
|
||||
# Extended properties are copied when splitting a recurrence Google side.
|
||||
# Hence, we may have two Google recurrences linked to the same Odoo id.
|
||||
# Therefore, we only consider Odoo records without google id when trying
|
||||
# to match events.
|
||||
o_ids = odoo_events.exists().filtered(lambda e: not e.google_id).ids
|
||||
for e in self:
|
||||
odoo_id = e._meta_odoo_id(env.cr.dbname)
|
||||
if odoo_id in o_ids:
|
||||
e._events[e.id]['_odoo_id'] = odoo_id
|
||||
|
||||
def _load_odoo_ids_from_db(self, env, model):
|
||||
odoo_events = model.with_context(active_test=False)._from_google_ids(self.ids)
|
||||
mapping = {e.google_id: e.id for e in odoo_events} # {google_id: odoo_id}
|
||||
existing_google_ids = odoo_events.mapped('google_id')
|
||||
for e in self:
|
||||
odoo_id = mapping.get(e.id)
|
||||
if odoo_id:
|
||||
e._events[e.id]['_odoo_id'] = odoo_id
|
||||
return self.filter(lambda e: e.id in existing_google_ids)
|
||||
|
||||
|
||||
def owner(self, env):
|
||||
# Owner/organizer could be desynchronised between Google and Odoo.
|
||||
# Let userA, userB be two new users (never synced to Google before).
|
||||
# UserA creates an event in Odoo (they are the owner) but userB syncs first.
|
||||
# There is no way to insert the event into userA's calendar since we don't have
|
||||
# any authentication access. The event is therefore inserted into userB's calendar
|
||||
# (they are the organizer in Google). The "real" owner (in Odoo) is stored as an
|
||||
# extended property. There is currently no support to "transfert" ownership when
|
||||
# userA syncs their calendar the first time.
|
||||
real_owner_id = self.extendedProperties and self.extendedProperties.get('shared', {}).get('%s_owner_id' % env.cr.dbname)
|
||||
try:
|
||||
# If we create an event without user_id, the event properties will be 'false'
|
||||
# and python will interpret this a a NoneType, that's why we have the 'except TypeError'
|
||||
real_owner_id = int(real_owner_id)
|
||||
except (ValueError, TypeError):
|
||||
real_owner_id = False
|
||||
real_owner = real_owner_id and env['res.users'].browse(real_owner_id) or env['res.users']
|
||||
if real_owner_id and real_owner.exists():
|
||||
return real_owner
|
||||
elif self.organizer and self.organizer.get('self'):
|
||||
return env.user
|
||||
elif self.organizer and self.organizer.get('email'):
|
||||
# In Google: 1 email = 1 user; but in Odoo several users might have the same email :/
|
||||
org_email = email_normalize(self.organizer.get('email'))
|
||||
return env['res.users'].search([('email_normalized', '=', org_email)], limit=1)
|
||||
else:
|
||||
return env['res.users']
|
||||
|
||||
def filter(self, func) -> 'GoogleEvent':
|
||||
return GoogleEvent(e for e in self if func(e))
|
||||
|
||||
def clear_type_ambiguity(self, env):
|
||||
ambiguous_events = self.filter(GoogleEvent._is_type_ambiguous)
|
||||
recurrences = ambiguous_events._load_odoo_ids_from_db(env, env['calendar.recurrence'])
|
||||
for recurrence in recurrences:
|
||||
self._events[recurrence.id]['recurrence'] = True
|
||||
for event in ambiguous_events - recurrences:
|
||||
self._events[event.id]['recurrence'] = False
|
||||
|
||||
def is_recurrence(self):
|
||||
if self._is_type_ambiguous():
|
||||
_logger.warning("Ambiguous event type: cannot accurately tell whether a cancelled event is a recurrence or not")
|
||||
return bool(self.recurrence)
|
||||
|
||||
def is_recurrent(self):
|
||||
return bool(self.recurringEventId or self.is_recurrence())
|
||||
|
||||
def is_cancelled(self):
|
||||
return self.status == 'cancelled'
|
||||
|
||||
def is_recurrence_follower(self):
|
||||
return bool(not self.originalStartTime or self.originalStartTime == self.start)
|
||||
|
||||
def full_recurring_event_id(self):
|
||||
"""
|
||||
Give the complete identifier with elements
|
||||
in `id` and `recurringEventId`.
|
||||
:return: concatenation of the id created by the recurrence
|
||||
and the id created by the modification of a specific event
|
||||
:rtype: string if recurrent event and correct ids, `None` otherwise
|
||||
"""
|
||||
# Regex expressions to match elements (according to the google support [not documented]):
|
||||
# - ID: [a-zA-Z0-9]+
|
||||
# - RANGE: R[0-9]+T[0-9]+
|
||||
# - TIMESTAMP: [0-9]+T[0-9]+Z
|
||||
# With:
|
||||
# - id: 'ID_TIMESTAMP'
|
||||
# - recurringEventID: 'ID_RANGE'
|
||||
# Find: 'ID_RANGE_TIMESTAMP'
|
||||
if not self.is_recurrent():
|
||||
return None
|
||||
# Check if ids are the same
|
||||
id_value = re.match(r'(\w+_)', self.id)
|
||||
recurringEventId_value = re.match(r'(\w+_)', self.recurringEventId)
|
||||
if not id_value or not recurringEventId_value or id_value.group(1) != recurringEventId_value.group(1):
|
||||
return None
|
||||
ID_RANGE = re.search(r'\w+_R\d+T\d+', self.recurringEventId).group()
|
||||
TIMESTAMP = re.search(r'\d+T\d+Z', self.id).group()
|
||||
return f"{ID_RANGE}_{TIMESTAMP}"
|
||||
|
||||
def cancelled(self):
|
||||
return self.filter(lambda e: e.status == 'cancelled')
|
||||
|
||||
def exists(self, env) -> 'GoogleEvent':
|
||||
recurrences = self.filter(GoogleEvent.is_recurrence)
|
||||
events = self - recurrences
|
||||
recurrences.odoo_ids(env)
|
||||
events.odoo_ids(env)
|
||||
|
||||
return self.filter(lambda e: e._odoo_id)
|
||||
|
||||
def _is_type_ambiguous(self):
|
||||
"""For cancelled events/recurrences, Google only send the id and
|
||||
the cancelled status. There is no way to know if it was a recurrence
|
||||
or simple event."""
|
||||
return self.is_cancelled() and 'recurrence' not in self._events[self.id]
|
||||
|
||||
def _get_model(self, env):
|
||||
if all(e.is_recurrence() for e in self):
|
||||
return env['calendar.recurrence']
|
||||
if all(not e.is_recurrence() for e in self):
|
||||
return env['calendar.event']
|
||||
raise TypeError("Mixing Google events and Google recurrences")
|
||||
|
||||
def get_meeting_url(self):
|
||||
if not self.conferenceData:
|
||||
return False
|
||||
video_meeting = list(filter(lambda entryPoints: entryPoints.get('entryPointType') == 'video', self.conferenceData.get('entryPoints', [])))
|
||||
return video_meeting[0].get('uri') if video_meeting else False
|
||||
|
||||
def is_available(self):
|
||||
return self.transparency == 'transparent'
|
||||
|
||||
def get_odoo_event(self, env):
|
||||
if self._get_model(env)._name == 'calendar.event':
|
||||
return env['calendar.event'].browse(self.odoo_id(self.env))
|
||||
else:
|
||||
return env['calendar.recurrence'].browse(self.odoo_id(self.env)).base_event_id
|
||||
Loading…
Add table
Add a link
Reference in a new issue