Skip to content

Commit b099a04

Browse files
committed
Sync user data
1 parent 13693ec commit b099a04

File tree

5 files changed

+205
-28
lines changed

5 files changed

+205
-28
lines changed

models/db.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ class DataSyncRecord(models.Model):
6666
tenantId = models.CharField(null=True, max_length=255)
6767
query = models.TextField(null=True)
6868
deltaLink = models.TextField(null=True)
69-
updated = models.DateTimeField()
69+
updated = models.DateTimeField(null=True)
7070
class Meta:
71-
db_table = 'data_sync_record'
71+
db_table = 'data_sync_records'

models/migrations/0001_initial.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Generated by Django 2.0.5 on 2018-05-02 06:30
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.3 on 2018-05-04 12:29
3+
from __future__ import unicode_literals
24

35
from django.conf import settings
46
from django.db import migrations, models
@@ -33,10 +35,10 @@ class Migration(migrations.Migration):
3335
('tenantId', models.CharField(max_length=255, null=True)),
3436
('query', models.TextField(null=True)),
3537
('deltaLink', models.TextField(null=True)),
36-
('updated', models.DateTimeField()),
38+
('updated', models.DateTimeField(null=True)),
3739
],
3840
options={
39-
'db_table': 'data_sync_record',
41+
'db_table': 'data_sync_records',
4042
},
4143
),
4244
migrations.CreateModel(
@@ -63,7 +65,7 @@ class Migration(migrations.Migration):
6365
('department', models.CharField(max_length=255, null=True)),
6466
('mobilePhone', models.CharField(max_length=255, null=True)),
6567
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='models.Organization')),
66-
('user', models.OneToOneField(on_delete=None, to=settings.AUTH_USER_MODEL)),
68+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
6769
],
6870
options={
6971
'db_table': 'profiles',

webjobs/user_data_sync/models.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from peewee import MySQLDatabase, Model, BooleanField, CharField, ForeignKeyField
2+
from peewee import MySQLDatabase, Model, BooleanField, CharField, TextField, DateTimeField, ForeignKeyField
33

44
mysql_host = os.environ['MySQLHost']
55
mysql_port = int('3306')
@@ -32,18 +32,26 @@ class Profile(BaseModel):
3232
class Meta:
3333
db_table = "profiles"
3434

35-
db.connect()
35+
class DataSyncRecord(BaseModel):
36+
tenantId = CharField()
37+
query = TextField()
38+
deltaLink = TextField()
39+
updated = DateTimeField(null=True)
40+
class Meta:
41+
db_table = 'data_sync_records'
42+
43+
# db.connect()
3644

37-
organizations = Organization.select() \
38-
.where(Organization.isAdminConsented)
45+
# organizations = Organization.select() \
46+
# .where(Organization.isAdminConsented)
3947

40-
for org in organizations:
41-
print(org.name)
48+
# for org in organizations:
49+
# print(org.name)
4250

4351

44-
profiles = Profile.select()
52+
# profiles = Profile.select()
4553

46-
for profile in profiles:
47-
print(profile.o365Email)
54+
# for profile in profiles:
55+
# print(profile.o365Email)
4856

49-
db.close()
57+
# db.close()
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'''
2+
* * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
3+
* * See LICENSE in the project root for license information.
4+
'''
5+
6+
import json
7+
import requests
8+
9+
class HttpRequestFailed(Exception):
10+
def __init__(self, request, response):
11+
self._request = request
12+
self._response = response
13+
14+
@property
15+
def response(self):
16+
return self._request
17+
18+
@property
19+
def response(self):
20+
return self._response
21+
22+
class RestApiService(object):
23+
24+
def get_raw(self, url, token, headers=None):
25+
method = 'GET'
26+
s_headers = {}
27+
self._set_header_token(s_headers, token)
28+
if headers:
29+
s_headers.update(headers)
30+
response = self._send(method, url, s_headers)
31+
return response.text
32+
33+
def get_json(self, url, token, headers=None):
34+
method = 'GET'
35+
s_headers = {'Accept': 'application/json',
36+
'Content-Type': 'application/json'}
37+
self._set_header_token(s_headers, token)
38+
if headers:
39+
s_headers.update(headers)
40+
response = self._send(method, url, s_headers)
41+
return json.loads(response.text)
42+
43+
def get_img(self, url, token, headers=None):
44+
method = 'GET'
45+
s_headers = {'content-type': 'image/jpeg'}
46+
self._set_header_token(s_headers, token)
47+
if headers:
48+
s_headers.update(headers)
49+
response = self._send(method, url, s_headers)
50+
return response.content
51+
52+
def get_object_list(self, url, token, key='value', headers=None, model=None, next_key=''):
53+
content = self.get_json(url, token, headers)
54+
entity_list = []
55+
next_link = ''
56+
if content and model:
57+
value_list = content[key]
58+
for value in value_list:
59+
entity = model(value)
60+
entity_list.append(entity)
61+
if next_key:
62+
next_link = content.get(next_key, '')
63+
if next_key:
64+
return entity_list, next_link
65+
else:
66+
return entity_list
67+
68+
def get_object(self, url, token, headers=None, model=None):
69+
content = self.get_raw(url, token, headers)
70+
if content and model:
71+
value = json.loads(content)
72+
return model(value)
73+
return None
74+
75+
def delete(self, url, token, headers=None):
76+
method = 'DELETE'
77+
s_headers = {'Accept': 'application/json',
78+
'Content-Type': 'application/json'}
79+
self._set_header_token(s_headers, token)
80+
if headers:
81+
s_headers.update(headers)
82+
self._send(method, url, s_headers)
83+
84+
def post_json(self, url, token, headers=None, data=None):
85+
method = 'POST'
86+
s_headers = {'Accept': 'application/json',
87+
'Content-Type': 'application/json'}
88+
self._set_header_token(s_headers, token)
89+
if headers:
90+
s_headers.update(headers)
91+
return self._send(method, url, s_headers, json.dumps(data))
92+
93+
def put_file(self, url, token, file=None):
94+
s_headers = {'Content-Type': 'application/octet-stream'}
95+
self._set_header_token(s_headers, token)
96+
method = 'PUT'
97+
return json.loads(self._send(method, url, s_headers,file.chunks()).text)
98+
99+
def _set_header_token(self, headers, token):
100+
key = 'Authorization'
101+
value = 'Bearer {0}'.format(token)
102+
headers[key] = value
103+
104+
def _send(self, method, url, headers, data=None):
105+
session = requests.Session()
106+
request = requests.Request(method, url, headers, data=data)
107+
prepped = request.prepare()
108+
response = session.send(prepped)
109+
if response.status_code < 200 or response.status_code > 299:
110+
raise HttpRequestFailed(request, response)
111+
return response
112+

webjobs/user_data_sync/start.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import os
22
import adal
3+
import datetime
4+
35
from OpenSSL import crypto
46
from cryptography.hazmat.primitives import serialization
57

8+
from rest_api_service import RestApiService
9+
import models
610

711
client_id = os.environ['ClientId']
812
client_cert_path = os.environ['ClientCertificatePath']
@@ -11,6 +15,8 @@
1115
aad_instance = 'https://login.microsoftonline.com/'
1216
ms_graph_resource = 'https://graph.microsoft.com'
1317

18+
users_query = "/users/delta?$select=jobTitle,department,mobilePhone"
19+
1420
# load certificate and thumbprint
1521
with open(client_cert_path, 'rb') as cert_file:
1622
pkcs12 = crypto.load_pkcs12(cert_file.read(), client_cert_password)
@@ -28,15 +34,64 @@
2834
.replace(':', '')
2935

3036
# get access token
31-
tenant_id = '64446b5c-6d85-4d16-9ff2-94eddc0c2439'
32-
authority = aad_instance + tenant_id
33-
auth_context = adal.AuthenticationContext(authority)
34-
35-
import pdb; pdb.set_trace()
36-
token = auth_context.acquire_token_with_client_certificate(
37-
ms_graph_resource,
38-
client_id,
39-
private_key,
40-
thumbprint)
41-
42-
print(token['accessToken'])
37+
38+
39+
models.db.connect()
40+
41+
42+
url = ms_graph_resource + '/v1.0/users/delta?$select=jobTitle,department,mobilePhone'
43+
44+
rest_api_service = RestApiService()
45+
46+
47+
organizations = models.Organization.select().where(models.Organization.isAdminConsented)
48+
for organization in organizations:
49+
50+
print('Sync tenant ' + organization.name)
51+
52+
data_sync_record, created = models.DataSyncRecord.get_or_create(
53+
tenantId = organization.tenantId,
54+
query = users_query)
55+
56+
if created:
57+
url = ms_graph_resource + '/v1.0' + users_query
58+
else:
59+
url = data_sync_record.deltaLink
60+
61+
62+
authority = aad_instance + organization.tenantId
63+
auth_context = adal.AuthenticationContext(authority, api_version=None)
64+
65+
token = auth_context.acquire_token_with_client_certificate(
66+
ms_graph_resource,
67+
client_id,
68+
private_key,
69+
thumbprint)
70+
71+
access_token = token['accessToken']
72+
73+
while True:
74+
res = rest_api_service.get_json(url, access_token)
75+
users = res['value']
76+
77+
for user in users:
78+
profile = models.Profile.get_or_none(models.Profile.o365UserId == user['id'])
79+
if profile:
80+
profile.jobTitle = user['jobTitle']
81+
profile.department = user['department']
82+
profile.mobilePhone = user['mobilePhone']
83+
print('update user: ' + profile.o365Email)
84+
profile.save()
85+
86+
next_link = res.get('@odata.nextLink')
87+
if next_link:
88+
url = next_link
89+
else:
90+
break;
91+
92+
93+
data_sync_record.deltaLink = res.get('@odata.deltaLink')
94+
data_sync_record.updated = datetime.datetime.now()
95+
data_sync_record.save()
96+
97+
models.db.close()

0 commit comments

Comments
 (0)