@@ -37,6 +37,7 @@ import { illinoisNetId, withRoles, withTags } from "api/components/index.js";
3737import { getKey , setKey } from "api/functions/redisCache.js" ;
3838import { AppRoles } from "common/roles.js" ;
3939import { unmarshall } from "@aws-sdk/util-dynamodb" ;
40+ import { verifyUiucAccessToken } from "api/functions/uin.js" ;
4041
4142const membershipPlugin : FastifyPluginAsync = async ( fastify , _options ) => {
4243 await fastify . register ( rawbody , {
@@ -81,6 +82,150 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
8182 duration : 30 ,
8283 rateLimitIdentifier : "membership" ,
8384 } ) ;
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+ ) ;
84229 fastify . withTypeProvider < FastifyZodOpenApiTypeProvider > ( ) . get (
85230 "/:netId" ,
86231 {
0 commit comments