@@ -37,6 +37,7 @@ import { illinoisNetId, withRoles, withTags } from "api/components/index.js";
37
37
import { getKey , setKey } from "api/functions/redisCache.js" ;
38
38
import { AppRoles } from "common/roles.js" ;
39
39
import { unmarshall } from "@aws-sdk/util-dynamodb" ;
40
+ import { verifyUiucAccessToken } from "api/functions/uin.js" ;
40
41
41
42
const membershipPlugin : FastifyPluginAsync = async ( fastify , _options ) => {
42
43
await fastify . register ( rawbody , {
@@ -81,6 +82,150 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
81
82
duration : 30 ,
82
83
rateLimitIdentifier : "membership" ,
83
84
} ) ;
85
+ fastify . withTypeProvider < FastifyZodOpenApiTypeProvider > ( ) . get (
86
+ "/" ,
87
+ {
88
+ schema : withTags ( [ "Membership" ] , {
89
+ querystring : z . object ( {
90
+ list : z . string ( ) . min ( 1 ) . optional ( ) . meta ( {
91
+ description :
92
+ "Membership list to check from (defaults to ACM Paid Member list)." ,
93
+ } ) ,
94
+ } ) ,
95
+ headers : z . object ( {
96
+ "x-uiuc-token" : z . jwt ( ) . min ( 1 ) . meta ( {
97
+ description :
98
+ "An access token for the user in the UIUC Entra ID tenant." ,
99
+ } ) ,
100
+ } ) ,
101
+ summary :
102
+ "Authenticated check ACM @ UIUC paid membership (or partner organization membership) status." ,
103
+ response : {
104
+ 200 : {
105
+ description : "List membership status." ,
106
+ content : {
107
+ "application/json" : {
108
+ schema : z
109
+ . object ( {
110
+ netId : illinoisNetId ,
111
+ list : z . optional ( z . string ( ) . min ( 1 ) ) ,
112
+ isPaidMember : z . boolean ( ) ,
113
+ } )
114
+ . meta ( {
115
+ example : {
116
+ netId : "rjjones" ,
117
+ isPaidMember : false ,
118
+ } ,
119
+ } ) ,
120
+ } ,
121
+ } ,
122
+ } ,
123
+ } ,
124
+ } ) ,
125
+ } ,
126
+ async ( request , reply ) => {
127
+ const accessToken = request . headers [ "x-uiuc-token" ] ;
128
+ const verifiedData = await verifyUiucAccessToken ( {
129
+ accessToken,
130
+ logger : request . log ,
131
+ } ) ;
132
+ const { userPrincipalName : upn , givenName, surname } = verifiedData ;
133
+ const netId = upn . replace ( "@illinois.edu" , "" ) ;
134
+ if ( netId . includes ( "@" ) ) {
135
+ request . log . error (
136
+ `Found UPN ${ upn } which cannot be turned into NetID via simple replacement.` ,
137
+ ) ;
138
+ throw new ValidationError ( {
139
+ message : "ID token could not be parsed." ,
140
+ } ) ;
141
+ }
142
+ const list = request . query . list || "acmpaid" ;
143
+ const cacheKey = `membership:${ netId } :${ list } ` ;
144
+ const result = await getKey < { isMember : boolean } > ( {
145
+ redisClient : fastify . redisClient ,
146
+ key : cacheKey ,
147
+ logger : request . log ,
148
+ } ) ;
149
+ if ( result ) {
150
+ return reply . header ( "X-ACM-Data-Source" , "cache" ) . send ( {
151
+ netId,
152
+ list : list === "acmpaid" ? undefined : list ,
153
+ isPaidMember : result . isMember ,
154
+ } ) ;
155
+ }
156
+ if ( list !== "acmpaid" ) {
157
+ const isMember = await checkExternalMembership (
158
+ netId ,
159
+ list ,
160
+ fastify . dynamoClient ,
161
+ ) ;
162
+ await setKey ( {
163
+ redisClient : fastify . redisClient ,
164
+ key : cacheKey ,
165
+ data : JSON . stringify ( { isMember } ) ,
166
+ expiresIn : MEMBER_CACHE_SECONDS ,
167
+ logger : request . log ,
168
+ } ) ;
169
+ return reply . header ( "X-ACM-Data-Source" , "dynamo" ) . send ( {
170
+ netId,
171
+ list,
172
+ isPaidMember : isMember ,
173
+ } ) ;
174
+ }
175
+ const isDynamoMember = await checkPaidMembershipFromTable (
176
+ netId ,
177
+ fastify . dynamoClient ,
178
+ ) ;
179
+ if ( isDynamoMember ) {
180
+ await setKey ( {
181
+ redisClient : fastify . redisClient ,
182
+ key : cacheKey ,
183
+ data : JSON . stringify ( { isMember : true } ) ,
184
+ expiresIn : MEMBER_CACHE_SECONDS ,
185
+ logger : request . log ,
186
+ } ) ;
187
+ return reply
188
+ . header ( "X-ACM-Data-Source" , "dynamo" )
189
+ . send ( { netId, isPaidMember : true } ) ;
190
+ }
191
+ const entraIdToken = await getEntraIdToken ( {
192
+ clients : await getAuthorizedClients ( ) ,
193
+ clientId : fastify . environmentConfig . AadValidClientId ,
194
+ secretName : genericConfig . EntraSecretName ,
195
+ logger : request . log ,
196
+ } ) ;
197
+ const paidMemberGroup = fastify . environmentConfig . PaidMemberGroupId ;
198
+ const isAadMember = await checkPaidMembershipFromEntra (
199
+ netId ,
200
+ entraIdToken ,
201
+ paidMemberGroup ,
202
+ ) ;
203
+ if ( isAadMember ) {
204
+ await setKey ( {
205
+ redisClient : fastify . redisClient ,
206
+ key : cacheKey ,
207
+ data : JSON . stringify ( { isMember : true } ) ,
208
+ expiresIn : MEMBER_CACHE_SECONDS ,
209
+ logger : request . log ,
210
+ } ) ;
211
+ reply
212
+ . header ( "X-ACM-Data-Source" , "aad" )
213
+ . send ( { netId, isPaidMember : true } ) ;
214
+ await setPaidMembershipInTable ( netId , fastify . dynamoClient ) ;
215
+ return ;
216
+ }
217
+ await setKey ( {
218
+ redisClient : fastify . redisClient ,
219
+ key : cacheKey ,
220
+ data : JSON . stringify ( { isMember : false } ) ,
221
+ expiresIn : MEMBER_CACHE_SECONDS ,
222
+ logger : request . log ,
223
+ } ) ;
224
+ return reply
225
+ . header ( "X-ACM-Data-Source" , "aad" )
226
+ . send ( { netId, isPaidMember : false } ) ;
227
+ } ,
228
+ ) ;
84
229
fastify . withTypeProvider < FastifyZodOpenApiTypeProvider > ( ) . get (
85
230
"/:netId" ,
86
231
{
0 commit comments