Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/google shared drives #561

Merged
merged 46 commits into from
May 15, 2024
Merged
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
20bfe16
initial shared drive record models
mikeseibel Feb 26, 2024
005774b
mock shared drive data and initial templating
mikeseibel Feb 29, 2024
fc173ff
shared drive api tests
mikeseibel Feb 29, 2024
d002f3e
shared drive loader upsert
mikeseibel Feb 29, 2024
314ab80
Merge branch 'develop' into feature/google-shared-drives
mikeseibel Mar 19, 2024
da31dd4
Merge branch 'develop' into feature/google-shared-drives
mikeseibel Mar 20, 2024
485b97d
itbill restclient
mikeseibel Apr 9, 2024
72f3bfe
shared drive record to unique drive
mikeseibel Apr 9, 2024
cc60f54
random key_remote
mikeseibel Apr 9, 2024
4e25613
layout interactions align with wireframes
mikeseibel Apr 27, 2024
e56ff52
Merge pull request #545 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel Apr 29, 2024
2a61d0a
Merge branch 'develop' into feature/google-shared-drives
mikeseibel Apr 30, 2024
c0fdf09
mock dates
mikeseibel Apr 30, 2024
23d6bdb
itbill payload integration
mikeseibel May 4, 2024
8e6bad7
Merge pull request #547 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 4, 2024
ce4d1b8
subscription relationships
mikeseibel May 4, 2024
74c8d5e
Merge pull request #548 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 4, 2024
42daf34
shared drive email warnings and tests
mikeseibel May 8, 2024
d4d5a11
shared drive notification templating
mikeseibel May 8, 2024
e9dbd64
Merge pull request #549 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 8, 2024
e6c7768
shared drive warning email tweaking
mikeseibel May 8, 2024
1923ad2
Merge pull request #550 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 8, 2024
8649e0c
misnamed metho
mikeseibel May 8, 2024
8b1dc57
Merge pull request #551 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 8, 2024
1978096
consolidate notification logic, add access lifecycle warnings
mikeseibel May 9, 2024
3182223
Merge pull request #552 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 9, 2024
9de0681
setting for conditional app exposure, prod value to prevent unintenti…
mikeseibel May 10, 2024
2acac2d
Merge pull request #553 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 10, 2024
a88d308
support notification display bug
mikeseibel May 10, 2024
6652531
Merge pull request #554 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 10, 2024
8f5acce
improve notification testing
mikeseibel May 10, 2024
8c4bb16
Merge pull request #555 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 10, 2024
e459a50
unused constants
mikeseibel May 10, 2024
d8d8e1e
Merge pull request #556 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 10, 2024
3c0a538
access notice tweaks
mikeseibel May 10, 2024
8212fa3
Merge pull request #557 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 10, 2024
0c028c7
tabs as links
mikeseibel May 14, 2024
ca971ba
Merge pull request #559 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 14, 2024
3be16be
over quota unsubsidized warning, handlebars logic cleanup
mikeseibel May 15, 2024
e566292
a11y updates
wmwash May 15, 2024
d5673e3
feature branch merge
mikeseibel May 15, 2024
98554c7
Merge pull request #560 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 15, 2024
25b8a63
pycodestyle
mikeseibel May 15, 2024
83aa449
Merge pull request #562 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 15, 2024
f70353d
jshint
mikeseibel May 15, 2024
d8ea610
Merge pull request #563 from uw-it-aca/work/google-shared-drives-mikes
mikeseibel May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
shared drive email warnings and tests
  • Loading branch information
mikeseibel committed May 8, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 42daf3497c056be03d8a443e148d77019fe2b3f7
7 changes: 7 additions & 0 deletions endorsement/dao/shared_drive.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,13 @@
r'^(?P<netid>[^@]+)@(uw|(u\.)?washington)\.edu$', re.I)


def shared_drive_lifecycle_expired(drive_record):
"""
Set lifecycle to expired for shared drive
"""
logger.info(f"Shared drive {drive_record} lifecycle expired")


def load_shared_drives_from_csv(file_path):
"""
populate shared drive models
22 changes: 11 additions & 11 deletions endorsement/fixtures/test_data/member.json
Original file line number Diff line number Diff line change
@@ -2,54 +2,54 @@
"model": "endorsement.member",
"pk": 1,
"fields": {
"name": "javerage"}},
"netid": "javerage"}},
{
"model": "endorsement.member",
"pk": 2,
"fields": {
"name": "endorsee7"}},
"netid": "endorsee7"}},
{
"model": "endorsement.member",
"pk": 3,
"fields": {
"name": "[email protected]"}},
"netid": "[email protected]"}},
{
"model": "endorsement.member",
"pk": 4,
"fields": {
"name": "endorsee6"}},
"netid": "endorsee6"}},
{
"model": "endorsement.member",
"pk": 5,
"fields": {
"name": "jstaff"}},
"netid": "jstaff"}},
{
"model": "endorsement.member",
"pk": 6,
"fields": {
"name": "jinter"}},
"netid": "jinter"}},
{
"model": "endorsement.member",
"pk": 7,
"fields": {
"name": "endorsee5"}},
"netid": "endorsee5"}},
{
"model": "endorsement.member",
"pk": 8,
"fields": {
"name": "endorsee4"}},
"netid": "endorsee4"}},
{
"model": "endorsement.member",
"pk": 9,
"fields": {
"name": "endorsee3"}},
"netid": "endorsee3"}},
{
"model": "endorsement.member",
"pk": 10,
"fields": {
"name": "endorsee2"}},
"netid": "endorsee2"}},
{
"model": "endorsement.member",
"pk": 11,
"fields": {
"name": "endorsee1"}}]
"netid": "endorsee1"}}]
2 changes: 1 addition & 1 deletion endorsement/management/commands/expire_endorsees.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from django.core.management.base import BaseCommand
from django.core.mail import mail_managers
from django.template import loader
from endorsement.policy import endorsements_to_expire
from endorsement.policy.endorsement import endorsements_to_expire
from endorsement.dao.endorse import clear_endorsement
import logging
import urllib3
23 changes: 23 additions & 0 deletions endorsement/migrations/0028_auto_20240506_1259.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.25 on 2024-05-06 19:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('endorsement', '0027_auto_20240503_1436'),
]

operations = [
migrations.RenameField(
model_name='member',
old_name='name',
new_name='netid',
),
migrations.AlterField(
model_name='shareddrive',
name='members',
field=models.ManyToManyField(blank=True, to='endorsement.SharedDriveMember'),
),
]
20 changes: 13 additions & 7 deletions endorsement/models/shared_drive.py
Original file line number Diff line number Diff line change
@@ -11,19 +11,19 @@


class MemberManager(models.Manager):
def get_member(self, name):
member, _ = self.get_or_create(name=name)
def get_member(self, netid):
member, _ = self.get_or_create(netid=netid)
return member


class Member(ExportModelOperationsMixin('member'), models.Model):
"""
Member represents user associated with a shared drive
"""
name = models.CharField(max_length=128)
netid = models.CharField(max_length=128)

def json_data(self):
return self.name
return self.netid

objects = MemberManager()

@@ -52,7 +52,7 @@ class SharedDriveMember(

def json_data(self):
return {
"name": self.member.json_data(),
"netid": self.member.json_data(),
"role": self.role.json_data()
}

@@ -84,6 +84,9 @@ def json_data(self):
"is_subsidized": self.is_subsidized
}

def __str__(self):
return json.dumps(self.json_data())


class SharedDrive(ExportModelOperationsMixin('shared_drive'), models.Model):
"""
@@ -94,9 +97,12 @@ class SharedDrive(ExportModelOperationsMixin('shared_drive'), models.Model):
drive_name = models.CharField(max_length=128)
drive_quota = models.ForeignKey(SharedDriveQuota, on_delete=models.PROTECT)
drive_usage = models.IntegerField(null=True)
members = models.ManyToManyField(SharedDriveMember)
members = models.ManyToManyField(SharedDriveMember, blank=True)
query_date = models.DateTimeField(null=True)

def get_members(self):
return [m.member.netid for m in self.members.all()]

def json_data(self):
return {
"drive_id": self.drive_id,
@@ -142,7 +148,7 @@ def __str__(self):
class SharedDriveRecordManager(models.Manager):
def get_member_drives(self, member_netid, drive_id=None):
parms = {
"shared_drive__members__member__name": member_netid,
"shared_drive__members__member__netid": member_netid,
"is_deleted__isnull": True}

if drive_id:
1 change: 1 addition & 0 deletions endorsement/notifications/access.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
from endorsement.models import AccessRecord
from endorsement.dao.notification import send_notification
from endorsement.dao.accessors import get_accessor_email
from endorsement.exceptions import EmailFailureException
from django.template import loader, Template, Context
import logging

3 changes: 2 additions & 1 deletion endorsement/notifications/endorsement.py
Original file line number Diff line number Diff line change
@@ -8,9 +8,10 @@
from endorsement.dao.user import get_endorsee_email_model
from endorsement.dao import display_datetime
from endorsement.dao.endorse import clear_endorsement
from endorsement.policy import endorsements_to_warn
from endorsement.policy.endorsement import endorsements_to_warn
from endorsement.util.email import uw_email_address
from endorsement.util.string import listed_list
from endorsement.exceptions import EmailFailureException
from django.template import loader, Template, Context
from django.utils import timezone
import re
70 changes: 70 additions & 0 deletions endorsement/notifications/shared_drive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2024 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

from endorsement.dao.notification import send_notification
from endorsement.models import SharedDriveRecord
from endorsement.dao.user import get_endorsee_email_model
from endorsement.dao import display_datetime
from endorsement.policy.shared_drive import (
shared_drives_to_warn, expiration_warning,
DEFAULT_SHARED_DRIVE_LIFETIME)
from endorsement.util.email import uw_email_address
from endorsement.exceptions import EmailFailureException
from django.template import loader, Template, Context
from django.utils import timezone
import re
import logging


logger = logging.getLogger(__name__)


def _email_template(template_name):
return "email/shared_drive/{}".format(template_name)


def _create_expire_notice_message(notice_level, lifetime, drive):
context = {
'drive': drive,
'lifetime': lifetime,
'notice_time': expiration_warning(notice_level)
}

if notice_level < 4:
subject = ("Action Required: Shared Drive "
"service will expire soon")
text_template = _email_template("notice_warning.txt")
html_template = _email_template("notice_warning.html")
else:
subject = "Action Required: Shared Drive services have expired"
text_template = _email_template("notice_warning_final.txt")
html_template = _email_template("notice_warning_final.html")

return (subject,
loader.render_to_string(text_template, context),
loader.render_to_string(html_template, context))


def warn_members(notice_level):
drives = shared_drives_to_warn(notice_level)
lifetime = DEFAULT_SHARED_DRIVE_LIFETIME

for drive in drives:
try:
members = [uw_email_address(netid) for (
netid) in drive.shared_drive.get_members()]
(subject,
text_body,
html_body) = _create_expire_notice_message(
notice_level, lifetime, drive)
send_notification(
members, subject, text_body, html_body,
"Shared Drive Warning")

sent_date = {
'datetime_notice_{}_emailed'.format(
notice_level): timezone.now()
}
drives.update(**sent_date)
except EmailFailureException as ex:
pass
125 changes: 125 additions & 0 deletions endorsement/policy/access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright 2024 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

"""
Office Mailbox Access lifecycle policy

Basic notions:
* Intial and subsequent warnings are sent prior to expiration.
* The expiration clock starts on the date of the first warning notice.
"""
from django.utils import timezone
from django.db.models import Q
from endorsement.models import AccessRecord
from endorsement.dao.access import revoke_access
from datetime import timedelta


# Default lifecycle day counts
DEFAULT_ACCESS_LIFETIME = 365
DEFAULT_ACCESS_GRACETIME = 90
PRIOR_DAYS_NOTICE_WARNING_1 = 90
PRIOR_DAYS_NOTICE_WARNING_2 = 30
PRIOR_DAYS_NOTICE_WARNING_3 = 7
PRIOR_DAYS_NOTICE_WARNING_4 = 0


def accessees_to_warn(level):
"""
"""
return _accessees_to_warn(timezone.now(), level)


def _accessees_to_warn(now, level):
"""
Gather records to receive expiration warning messages where
level is the index of the warning message: first, second and so forth

The expiration clock starts on the date of the first warning notice
"""
if level < 1 or level > 4:
raise Exception('bad warning level {}'.format(level))

q = Q()

# select on appropriate time span for warning notice index (level)
days_prior = _expiration_warning(level)
if days_prior is None:
return

if level == 1:
granted = now - timedelta(
days=DEFAULT_ACCESS_LIFETIME - days_prior)
q = q | Q(datetime_granted__lt=granted,
datetime_notice_1_emailed__isnull=True,
is_deleted__isnull=True)
else:
prev_days_prior = _expiration_warning(level - 1)
prev_warning_date = now - timedelta(
days=prev_days_prior - days_prior)

if level == 2:
q = q | Q(datetime_notice_1_emailed__lt=prev_warning_date,
datetime_notice_2_emailed__isnull=True,
is_deleted__isnull=True)
elif level == 3:
q = q | Q(datetime_notice_2_emailed__lt=prev_warning_date,
datetime_notice_3_emailed__isnull=True,
is_deleted__isnull=True)
else:
q = q | Q(datetime_notice_3_emailed__lt=prev_warning_date,
datetime_notice_4_emailed__isnull=True,
is_deleted__isnull=True)

return AccessRecord.objects.filter(q)


def access_to_expire():
"""
Return query set of access records to expire
"""
return _access_to_expire(timezone.now())


def _access_to_expire(now):
"""
Return query set of accees to expire for each service
"""
q = Q()

expiration_date = now - timedelta(days=DEFAULT_ACCESS_GRACETIME)
q = q | Q(datetime_notice_4_emailed__lt=expiration_date,
datetime_notice_3_emailed__isnull=False,
datetime_notice_2_emailed__isnull=False,
datetime_notice_1_emailed__isnull=False,
is_deleted__isnull=True)

return AccessRecord.objects.filter(q)


def expire_office_access(gracetime, lifetime):
"""
"""
access = access_to_expire(gracetime, lifetime)
if len(access):
for a in acccess:
revoke_access(a)


def _expiration_warning(self, level=1):
"""
for the given warning message level, return days prior to
expiration that a warning should be sent.

level 1 is the first warning, level 2 the second and so on
to final warning at 0 days before expiration
"""
try:
return [
PRIOR_DAYS_NOTICE_WARNING_1,
PRIOR_DAYS_NOTICE_WARNING_2,
PRIOR_DAYS_NOTICE_WARNING_3,
PRIOR_DAYS_NOTICE_WARNING_4
][level - 1]
except IndexError:
return None
Loading