Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:

jobs:
context:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

outputs:
commit_hash: ${{ steps.context.outputs.commit_hash }}
Expand All @@ -29,7 +29,7 @@ jobs:
release_name: ${{ env.RELEASE_NAME }}

build:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

needs: context

Expand All @@ -40,7 +40,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.6
python-version: 3.8

- name: Run Python Linters
uses: uw-it-aca/actions/python-linters@main
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:

needs: [context, build]

runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

outputs:
context: ${{ steps.context.outputs.context }}
Expand Down Expand Up @@ -141,7 +141,7 @@ jobs:

needs: [context, build, deploy]

runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

steps:
- name: House Keeping
Expand Down
24 changes: 0 additions & 24 deletions docker/prod-values.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,6 @@ externalSecrets:
property: itbill-form-url-base-id
- name: itbill-form-url-sys-id
property: itbill-form-url-sys-id
- name: msca-oauth-token-url
property: msca-oauth-token-url
- name: msca-report-scope
property: msca-report-scope
- name: msca-client-id
property: msca-client-id
- name: msca-client-secret
property: msca-client-secret
- name: msca-subscription-key
property: msca-subscription-key
- name: provision.uw.edu-sql-secret
Expand Down Expand Up @@ -195,22 +187,6 @@ environmentVariablesSecrets:
name: ITBILL_FORM_URL_SYS_ID
secretName: provision.uw.edu-secrets
secretKey: itbill-form-url-sys-id
MSCA_OAUTH_TOKEN_URL:
name: MSCA_OAUTH_TOKEN_URL
secretName: provision.uw.edu-secrets
secretKey: msca-oauth-token-url
MSCAReportScope:
name: MSCA_REPORT_SCOPE
secretName: provision.uw.edu-secrets
secretKey: msca-report-scope
MSCAClientID:
name: MSCA_CLIENT_ID
secretName: provision.uw.edu-secrets
secretKey: msca-client-id
MSCAClientSecret:
name: MSCA_CLIENT_SECRET
secretName: provision.uw.edu-secrets
secretKey: msca-client-secret
MSCASubscriptionKey:
name: MSCA_SUBSCRIPTION_KEY
secretName: provision.uw.edu-secrets
Expand Down
6 changes: 0 additions & 6 deletions docker/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,6 @@
ITBILL_FORM_URL_BASE_ID=os.getenv('ITBILL_FORM_URL_BASE_ID')
ITBILL_FORM_URL_SYS_ID=os.getenv('ITBILL_FORM_URL_SYS_ID')

RESTCLIENTS_MSCA_OAUTH_TOKEN_URL = os.getenv("MSCA_OAUTH_TOKEN_URL")
RESTCLIENTS_MSCA_REPORT_SCOPE = os.getenv("MSCA_REPORT_SCOPE")
RESTCLIENTS_MSCA_CLIENT_ID = os.getenv("MSCA_CLIENT_ID")
RESTCLIENTS_MSCA_CLIENT_SECRET = os.getenv("MSCA_CLIENT_SECRET")


VALID_ENDORSER_GROUP = 'u_pplat_provisioners'
PROVISION_ADMIN_GROUP = 'u_acadev_provision_admin'
PROVISION_SUPPORT_GROUP = 'u_acadev_provision_support'
Expand Down
24 changes: 0 additions & 24 deletions docker/test-values.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,6 @@ externalSecrets:
property: itbill-form-url-base-id
- name: itbill-form-url-sys-id
property: itbill-form-url-sys-id
- name: msca-oauth-token-url
property: msca-oauth-token-url
- name: msca-report-scope
property: msca-report-scope
- name: msca-client-id
property: msca-client-id
- name: msca-client-secret
property: msca-client-secret
- name: email-host
property: email-host
- name: msca-subscription-key
Expand Down Expand Up @@ -192,22 +184,6 @@ environmentVariablesSecrets:
name: ITBILL_FORM_URL_SYS_ID
secretName: test.provision.uw.edu-secrets
secretKey: itbill-form-url-sys-id
MSCA_OAUTH_TOKEN_URL:
name: MSCA_OAUTH_TOKEN_URL
secretName: test.provision.uw.edu-secrets
secretKey: msca-oauth-token-url
MSCAReportScope:
name: MSCA_REPORT_SCOPE
secretName: test.provision.uw.edu-secrets
secretKey: msca-report-scope
MSCAClientID:
name: MSCA_CLIENT_ID
secretName: test.provision.uw.edu-secrets
secretKey: msca-client-id
MSCAClientSecret:
name: MSCA_CLIENT_SECRET
secretName: test.provision.uw.edu-secrets
secretKey: msca-client-secret
emailHost:
name: EMAIL_HOST
secretName: test.provision.uw.edu-secrets
Expand Down
2 changes: 0 additions & 2 deletions endorsement/management/commands/reconcile_shared_drives.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Copyright 2025 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

# Copyright 2024 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0
import logging
import sys

Expand Down
171 changes: 171 additions & 0 deletions endorsement/management/commands/restore_access_rights.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright 2025 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

from django.core.management.base import BaseCommand
from endorsement.models import AccessRecord, AccessRight, AccessRecordConflict
from endorsement.dao.access import (
get_accessee_model, store_access_record, set_delegate)
from endorsement.dao.office import get_office_accessor
from endorsement.exceptions import UnrecognizedUWNetid, UnrecognizedGroupID
from uw_msca.delegate import get_all_delegates, _msca_get_delegate_url
from uw_msca import get_resource
import json
import csv
import logging


logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class Command(BaseCommand):
help = "Restore Office365 mailbox access from MSCA"

def add_arguments(self, parser):
parser.add_argument(
'--commit',
action='store_true',
default=False,
help='Store access record changes',
)
parser.add_argument(
'--netid',
type=str,
help='Netid to restore rights for')
parser.add_argument(
'--csv',
type=str,
help='CSV of accessor netid, accessor name, access right')

def handle(self, *args, **options):
self.commit_changes = options['commit']
netid = options['netid']
csv_file = options['csv']
try:
if netid:
self.restore_netid_access(netid)
elif csv_file:
self.restore_csv_access(csv_file)
except Exception as ex:
logger.error("restore_access_rights: Exception: {}".format(ex))

def restore_netid_access(self, netid):
accessee = get_accessee_model(netid)
for delegate, right in self.get_delegates_for_netid(netid).items():
try:
self.fix_access_record(accessee, delegate, right)
except Exception as ex:
logger.info(f"ERROR: assign delegate {delegate}: {ex}")
continue

def get_delegates_for_netid(self, netid):
url = _msca_get_delegate_url(netid)
response = get_resource(url)
json_response = json.loads(response)
delegates = {}
for delegation in json_response:
if netid != delegation['netid']:
raise Exception("netid mismatch")
netid = delegation['netid']
delegations = delegation['delegates']
if isinstance(delegations, dict):
user = delegations['User']
if not user:
continue

rights = delegations['AccessRights']
delegates[user] = rights
elif isinstance(delegations, list):
for d in delegations:
user = d['User']
if not user:
continue

rights = d['AccessRights']
if isinstance(rights, list):
if len(rights) == 1:
delegates[user] = rights[0]
else:
raise Exception(f"multiple rights for "
f"{user}: {rights}")
elif isinstance(rights, str):
delegates[user] = rights
else:
raise Exception(f"unknown right type for {user}")
return delegates

def fix_access_record(self, accessee, delegate, right):
try:
accessor = get_office_accessor(delegate)
except Exception as ex:
raise Exception(f"ERROR: get accessor {delegate}: {ex}")

try:
ar = AccessRecord.objects.get(
accessee=accessee, accessor=accessor)
except AccessRecord.DoesNotExist:
raise Exception(f"ERROR: no record: mailbox {netid} "
f"delegate {delegate}")
try:
rr = AccessRight.objects.get(name=right)
except AccessRight.DoesNotExist:
raise Exception(f"ERROR: unknown right {right} ")

if ar.access_right != rr:
if self.commit_changes:
ar.access_right = rr
ar.save()

logger.info(
f"{'' if self.commit_changes else 'WOULD '}ASSIGN: "
f"mailbox {ar.accessee.netid} "
f"delegate {ar.accessor.name} "
f"right '{ar.access_right.display_name if (
self.commit_changes) else rr.display_name}'")

def reconcile_csv_access(self, csv_file):
delegations = {}
with open(csv_file, 'r') as f:
blank_reader = csv.reader(f)
for i, line in enumerate(blank_reader):
delegates = json.loads(line[1])
if not delegates or delegates == 'null':
continue

netid = line[0]
if netid not in delegations:
delegations[netid] = {}

if isinstance(delegates, dict):
user = delegates['User']
right = delegates['AccessRights']
delegations[netid][user] = right
else:
for d in delegates:
user = d['User']
right = d['AccessRights']
delegations[netid][user] = right

with open('/tmp/blanks_remaining.csv', 'r') as f:
blank_reader = csv.reader(f)
for i, line in enumerate(blank_reader):
netid = line[0]
if netid not in delegations:
logger.info(f"mailbox {netid} has no delegates")
continue

try:
accessee = get_accessee_model(netid)
except UnrecognizedUWNetid:
logger.info(f"ERROR: get accessee {netid}: unrecognized")
continue
except Exception as ex:
logger.info(f"ERROR: get accessee {netid}: {ex}")
continue

for delegate, right in delegations[netid].items():
try:
fix_access_record(accessee, delegate, right)
except Exception as ex:
logger.info(f"ERROR: assign delegate {delegate}: {ex}")
continue
10 changes: 7 additions & 3 deletions endorsement/notifications/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ def _create_accessee_expiration_notice(notice_level, access, policy):
}

if notice_level < 4:
subject = ("Action Required: Office 365 Shared Mailbox "
"service will expire soon")
subject = ("Action Required: Office 365 mailbox "
"permissions expiring soon")
text_template = _email_template("notice_warning.txt")
html_template = _email_template("notice_warning.html")
else:
Expand All @@ -104,7 +104,11 @@ def accessee_lifecycle_warning(notice_level):

for drive in drives:
try:
email = [uw_email_address(drive.accessee.netid)]
owner = get_owner_for_shared_netid(drive.accessee.netid)
if not owner:
owner = drive.accessee.netid

email = [uw_email_address(owner)]
(subject,
text_body,
html_body) = _create_accessee_expiration_notice(
Expand Down
Loading