Skip to content

Commit b013204

Browse files
authored
feat: [PPT-2420] added check calendar permission endpoint (#365)
1 parent 6a4c8bf commit b013204

File tree

9 files changed

+914
-74
lines changed

9 files changed

+914
-74
lines changed

OPENAPI_DOC.yml

Lines changed: 668 additions & 70 deletions
Large diffs are not rendered by default.

shard.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,11 @@ shards:
111111

112112
office365:
113113
git: https://github.com/placeos/office365.git
114-
version: 1.26.0
114+
version: 1.26.1
115115

116116
openssl_ext:
117117
git: https://github.com/spider-gazelle/openssl_ext.git
118-
version: 2.8.4
118+
version: 2.8.5
119119

120120
pars:
121121
git: https://github.com/spider-gazelle/pars.git

spec/controllers/calendars_spec.cr

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,99 @@ describe Calendars do
108108
bad_request = client.get(route, headers: headers).status_code
109109
bad_request.should eq(400)
110110
end
111+
112+
describe "#check_permission" do
113+
it "should return owner role when checking own calendar" do
114+
WebMock.stub(:post, "https://login.microsoftonline.com/bb89674a-238b-4b7d-91ec-6bebad83553a/oauth2/v2.0/token")
115+
.to_return(body: File.read("./spec/fixtures/tokens/o365_token.json"))
116+
117+
route = "#{CALENDARS_BASE}/dev@acaprojects.onmicrosoft.com/permission"
118+
response = client.get(route, headers: headers)
119+
response.status_code.should eq(200)
120+
121+
body = JSON.parse(response.body)
122+
body["has_access"].should eq(true)
123+
body["role"].should eq("owner")
124+
end
125+
126+
it "should return write access when user has write permission" do
127+
WebMock.stub(:post, "https://login.microsoftonline.com/bb89674a-238b-4b7d-91ec-6bebad83553a/oauth2/v2.0/token")
128+
.to_return(body: File.read("./spec/fixtures/tokens/o365_token.json"))
129+
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/pradeep%40domain.com/calendar/calendarPermissions")
130+
.to_return(body: File.read("./spec/fixtures/calendars/o365/calendar_permissions_write_access.json"))
131+
132+
route = "#{CALENDARS_BASE}/pradeep@domain.com/permission"
133+
response = client.get(route, headers: headers)
134+
response.status_code.should eq(200)
135+
136+
body = JSON.parse(response.body)
137+
body["has_access"].should eq(true)
138+
body["role"].should eq("write")
139+
end
140+
141+
it "should return delegate access when user has delegate permission" do
142+
WebMock.stub(:post, "https://login.microsoftonline.com/bb89674a-238b-4b7d-91ec-6bebad83553a/oauth2/v2.0/token")
143+
.to_return(body: File.read("./spec/fixtures/tokens/o365_token.json"))
144+
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/pradeep%40domain.com/calendar/calendarPermissions")
145+
.to_return(body: File.read("./spec/fixtures/calendars/o365/calendar_permissions_delegate_access.json"))
146+
147+
route = "#{CALENDARS_BASE}/pradeep@domain.com/permission"
148+
response = client.get(route, headers: headers)
149+
response.status_code.should eq(200)
150+
151+
body = JSON.parse(response.body)
152+
body["has_access"].should eq(true)
153+
body["role"].should eq("delegateWithoutPrivateEventAccess")
154+
end
155+
156+
it "should return no access when user has only read permission" do
157+
WebMock.stub(:post, "https://login.microsoftonline.com/bb89674a-238b-4b7d-91ec-6bebad83553a/oauth2/v2.0/token")
158+
.to_return(body: File.read("./spec/fixtures/tokens/o365_token.json"))
159+
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/pradeep%40domain.com/calendar/calendarPermissions")
160+
.to_return(body: File.read("./spec/fixtures/calendars/o365/calendar_permissions_read_only.json"))
161+
162+
route = "#{CALENDARS_BASE}/pradeep@domain.com/permission"
163+
response = client.get(route, headers: headers)
164+
response.status_code.should eq(200)
165+
166+
body = JSON.parse(response.body)
167+
body["has_access"].should eq(false)
168+
body["role"].should eq("read")
169+
end
170+
171+
it "should return no access when user is not in permissions list" do
172+
WebMock.stub(:post, "https://login.microsoftonline.com/bb89674a-238b-4b7d-91ec-6bebad83553a/oauth2/v2.0/token")
173+
.to_return(body: File.read("./spec/fixtures/tokens/o365_token.json"))
174+
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/pradeep%40domain.com/calendar/calendarPermissions")
175+
.to_return(body: File.read("./spec/fixtures/calendars/o365/calendar_permissions_no_access.json"))
176+
177+
route = "#{CALENDARS_BASE}/pradeep@domain.com/permission"
178+
response = client.get(route, headers: headers)
179+
response.status_code.should eq(200)
180+
181+
body = JSON.parse(response.body)
182+
body["has_access"].should eq(false)
183+
body["role"].should eq("none")
184+
end
185+
186+
it "should handle errors gracefully and return error role" do
187+
WebMock.stub(:post, "https://login.microsoftonline.com/bb89674a-238b-4b7d-91ec-6bebad83553a/oauth2/v2.0/token")
188+
.to_return(body: File.read("./spec/fixtures/tokens/o365_token.json"))
189+
# Stub with both encoded and unencoded versions to ensure match
190+
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/pradeep%40domain.com/calendar/calendarPermissions")
191+
.to_return(status: 500, body: "Internal Server Error")
192+
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/pradeep@domain.com/calendar/calendarPermissions")
193+
.to_return(status: 500, body: "Internal Server Error")
194+
195+
route = "#{CALENDARS_BASE}/pradeep@domain.com/permission"
196+
response = client.get(route, headers: headers)
197+
response.status_code.should eq(200)
198+
199+
body = JSON.parse(response.body)
200+
body["has_access"].should eq(false)
201+
body["role"].should eq("error")
202+
end
203+
end
111204
end
112205

113206
CALENDARS_BASE = Calendars.base_route
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('pradeep%40domain.com')/calendar/calendarPermissions",
3+
"value": [
4+
{
5+
"id": "RGVsZWdhdGVk",
6+
"role": "delegateWithoutPrivateEventAccess",
7+
"allowedRoles": [
8+
"freeBusyRead",
9+
"limitedRead",
10+
"read",
11+
"write",
12+
"delegateWithoutPrivateEventAccess"
13+
],
14+
"isRemovable": true,
15+
"isInsideOrganization": true,
16+
"emailAddress": {
17+
"name": "Developer",
18+
"address": "dev@acaprojects.onmicrosoft.com"
19+
}
20+
}
21+
]
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('pradeep%40domain.com')/calendar/calendarPermissions",
3+
"value": [
4+
{
5+
"id": "T3JnYW5pemF0aW9u",
6+
"role": "freeBusyRead",
7+
"allowedRoles": [
8+
"freeBusyRead",
9+
"limitedRead",
10+
"read",
11+
"write"
12+
],
13+
"isRemovable": false,
14+
"isInsideOrganization": true,
15+
"emailAddress": {
16+
"name": "My Organization",
17+
"address": ""
18+
}
19+
}
20+
]
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('pradeep%40domain.com')/calendar/calendarPermissions",
3+
"value": [
4+
{
5+
"id": "UmVhZE9ubHk",
6+
"role": "read",
7+
"allowedRoles": [
8+
"freeBusyRead",
9+
"limitedRead",
10+
"read"
11+
],
12+
"isRemovable": true,
13+
"isInsideOrganization": true,
14+
"emailAddress": {
15+
"name": "Developer",
16+
"address": "dev@acaprojects.onmicrosoft.com"
17+
}
18+
}
19+
]
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('pradeep%40domain.com')/calendar/calendarPermissions",
3+
"value": [
4+
{
5+
"id": "RGVsZWdhdGVk",
6+
"role": "write",
7+
"allowedRoles": [
8+
"freeBusyRead",
9+
"limitedRead",
10+
"read",
11+
"write"
12+
],
13+
"isRemovable": true,
14+
"isInsideOrganization": true,
15+
"emailAddress": {
16+
"name": "Developer",
17+
"address": "dev@acaprojects.onmicrosoft.com"
18+
}
19+
}
20+
]
21+
}

src/controllers/calendars.cr

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Calendars < Application
1414
current_tenant
1515
end
1616

17-
@[AC::Route::Filter(:before_action, except: [:index])]
17+
@[AC::Route::Filter(:before_action, except: [:index, :check_permission])]
1818
private def find_matching_calendars(
1919
@[AC::Param::Info(description: "a comma seperated list of calendar ids, recommend using `system_id` for resource calendars", example: "user@org.com,room2@resource.org.com")]
2020
calendars : String? = nil,
@@ -46,6 +46,47 @@ class Calendars < Application
4646
client.list_calendars(user.email)
4747
end
4848

49+
record PermissionCheck, has_access : Bool, role : String do
50+
include JSON::Serializable
51+
end
52+
53+
# Check if current user has write access to specified user's calendar
54+
@[AC::Route::GET("/:user_email/permission")]
55+
def check_permission(
56+
@[AC::Param::Info(description: "email or UPN of calendar owner", example: "foo@domain.com")]
57+
user_email : String,
58+
) : PermissionCheck
59+
current_user_email = user.email.downcase
60+
target_email = user_email.downcase
61+
62+
# User always has permission to their own calendar
63+
if current_user_email == target_email
64+
return PermissionCheck.new(has_access: true, role: "owner")
65+
end
66+
67+
# Get Office365 client and call calendarPermissions API
68+
if client.client_id == :office365
69+
o365_client = client.calendar.as(PlaceCalendar::Office365).client
70+
permissions_query = o365_client.list_calendar_permissions(target_email)
71+
72+
# Find current user in permissions list
73+
user_permission = permissions_query.value.find do |perm|
74+
perm.email_address.address.try(&.downcase) == current_user_email
75+
end
76+
77+
if user_permission && user_permission.can_edit?
78+
PermissionCheck.new(has_access: true, role: user_permission.role)
79+
else
80+
PermissionCheck.new(has_access: false, role: user_permission.try(&.role) || "none")
81+
end
82+
else
83+
PermissionCheck.new(has_access: false, role: "unsupported")
84+
end
85+
rescue ex
86+
Log.warn(exception: ex) { "failed to check calendar permission for #{target_email}" }
87+
PermissionCheck.new(has_access: false, role: "error")
88+
end
89+
4990
# checks for availability of matched calendars, returns a list of calendars with availability
5091
@[AC::Route::GET("/availability")]
5192
def availability(

src/controllers/events.cr

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,31 @@ class Events < Application
378378
if host_email == service_account
379379
return attendees.includes?(user_email)
380380
end
381-
!!get_user_calendars.find { |cal| cal.id.try(&.downcase) == host_email }
381+
382+
# Check if user has write access to the calendar (works for both Office365 and Google)
383+
return true if get_user_calendars.find { |cal| cal.id.try(&.downcase) == host_email }
384+
385+
# Check calendar permissions via calendarPermissions API (Office365 only)
386+
check_calendar_permission(user_email, host_email)
387+
end
388+
389+
protected def check_calendar_permission(user_email : String, host_email : String) : Bool
390+
# Only Office365 supports calendar permissions API
391+
return true unless client.client_id == :office365
392+
393+
o365_client = client.calendar.as(PlaceCalendar::Office365).client
394+
permissions_query = o365_client.list_calendar_permissions(host_email)
395+
396+
# Find current user in permissions list
397+
user_permission = permissions_query.value.find do |perm|
398+
perm.email_address.address.try(&.downcase) == user_email
399+
end
400+
401+
user_permission ? user_permission.can_edit? : false
402+
rescue ex
403+
# If we can't check permissions due to an error (network, API timeout, etc.), log it and allow operation.
404+
Log.warn(exception: ex) { "failed to check calendar permission for #{host_email}, allowing operation" }
405+
true
382406
end
383407

384408
# creates a new calendar event

0 commit comments

Comments
 (0)