Przeglądaj źródła

* Fixes for OAuth scopes declaration

Michaël Van Canneyt 1 tydzień temu
rodzic
commit
528dd4989d

+ 25 - 2
packages/fcl-openapi/src/fpopenapi.reader.pp

@@ -76,6 +76,7 @@ Type
     Function ReadObjectArray(aList : TFPObjectList; aGetObject : TCreateAndReadObjectFunc) : integer;overload;
     function ReadMapObject(aList: TNamedOpenAPIObjectList; aObjectReader : TReadObjectProc; aCheckBracket: Boolean=True): integer;
     Function ReadObject(aList : TStrings) : integer;
+    Function ReadStringMap(aList : TStrings) : integer;
     // objects
     Procedure ReadOpenAPI(aObj : TOpenAPI; aCheckBracket : Boolean = True); virtual; overload;
     Procedure ReadInfo(aObj : TInfo; aCheckBracket : Boolean = True); virtual; overload;
@@ -429,6 +430,28 @@ begin
     end;
 end;
 
+function TOpenAPIReader.ReadStringMap(aList: TStrings): integer;
+// Reads a JSON object as key-value pairs into TStrings (Name=Value format)
+var
+  aToken: TJSONToken;
+  aKey, aValue: string;
+begin
+  Result:=0;
+  CheckNextToken(tkCurlyBraceOpen);
+  aToken:=CheckNextToken([tkString,tkIdentifier,tkCurlyBraceClose]);
+  while Not (aToken in [tkEOF,tkCurlyBraceClose]) do
+    begin
+    aKey:=GetTokenString;
+    CheckNextToken(tkColon);
+    aValue:=ReadString;
+    aList.Add(aKey+'='+aValue);
+    inc(Result);
+    aToken:=CheckNextToken([tkComma,tkCurlyBraceClose]);
+    if aToken=tkComma then
+      aToken:=CheckNextToken([tkString,tkIdentifier]);
+    end;
+end;
+
 procedure TOpenAPIReader.ReadOpenAPI(aObj: TOpenAPI; aCheckBracket: Boolean);
 // On entry, we are on { token if aCheckBracket is false, before if it is true
 var
@@ -1750,7 +1773,7 @@ begin
     dikPropertyName:
       aObj.PropertyName:=Readstring;
     dikMapping:
-      ReadObject(aObj.Mapping);
+      ReadStringMap(aObj.Mapping);
     else
       aObj.Extensions.Add(aName,ReadJSONData);
     end;
@@ -1955,7 +1978,7 @@ begin
     ofkRefreshURL:
       aObj.RefreshURL:=Readstring;
     ofkScopes:
-      ReadObject(aObj.Scopes);
+      ReadStringMap(aObj.Scopes);
     else
       aObj.Extensions.Add(aName,ReadJSONData);
     end;

+ 28 - 3
packages/fcl-openapi/src/fpopenapi.writer.pp

@@ -71,6 +71,7 @@ Type
     Procedure WriteProperty(const aName : String; const aValue : TJSONSchema); inline;
     Procedure WriteProperty(const aName : String; const aValue : TJSONData); inline;
     procedure WriteStrings(const aKey : String; aList : TStrings) ;
+    procedure WriteStringMap(const aKey : String; aList : TStrings) ;
     procedure WriteExtensions(aObj : TJSONObject);
     procedure WriteObjectArray(const aKey: String; aList: TFPObjectList; aWriteObject: TWriteObjectFunc); overload;
     procedure WriteMapObject(const aKey : String; aList: TNamedOpenAPIObjectList; aObjectWriter : TWriteObjectFunc);
@@ -132,6 +133,7 @@ Type
     procedure WriteOAuthFlow(aObj : TOauthFlow); virtual; overload;
     procedure WriteOAuthFlow(const aKey : String; aObj : TOauthFlow); virtual; overload;
     procedure WriteOAuthFlows(aObj : TOAuthFlows); virtual; overload;
+    procedure WriteOAuthFlows(const aKey : String; aObj : TOAuthFlows); virtual; overload;
     procedure WriteHeader(aObj : THeader); virtual; overload;
     procedure WriteHeaderOrReference(aObj : THeaderOrReference); virtual; overload;
     procedure WriteHeaderOrReferenceMap(const aKey : string; aObj : THeaderOrReferenceMap); virtual; overload;
@@ -334,6 +336,22 @@ begin
   Writer.EndProperty();
 end;
 
+procedure TOpenAPIWriter.WriteStringMap(const aKey: String; aList: TStrings);
+// Writes TStrings as a JSON object with key-value pairs (Name=Value format)
+var
+  I: Integer;
+begin
+  Writer.StartProperty(aKey);
+  Writer.StartObject;
+  For I := 0 to aList.Count - 1 do
+    begin
+    Writer.NextElement;
+    Writer.WriteProperty(aList.Names[I], aList.ValueFromIndex[I]);
+    end;
+  Writer.EndObject;
+  Writer.EndProperty();
+end;
+
 procedure TOpenAPIWriter.WriteObjectArray(const aKey: String; aList: TFPObjectList; aWriteObject: TWriteObjectFunc);
 
 var
@@ -1314,7 +1332,7 @@ begin
       dikPropertyName:
         WriteProperty(lName,aObj.PropertyName);
       dikMapping:
-        WriteStrings(lName,aObj.Mapping);
+        WriteStringMap(lName,aObj.Mapping);
       end;
       end;
   if aObj.HasExtensions then
@@ -1376,7 +1394,7 @@ begin
       sskBearerFormat:
         WriteProperty(lName,aObj.BearerFormat);
       sskFlows:
-        WriteOAuthFlows(aObj.Flows);
+        WriteOAuthFlows(lName, aObj.Flows);
       sskOpenIdConnectUrl:
         WriteProperty(lName,aObj.OpenIdConnectUrl);
       end;
@@ -1434,6 +1452,13 @@ begin
     WriteExtensions(aObj.Extensions);
 end;
 
+procedure TOpenAPIWriter.WriteOAuthFlows(const aKey: String; aObj: TOAuthFlows);
+begin
+  StartObjectProp(aKey);
+  WriteOAuthFlows(aObj);
+  EndObjectProp;
+end;
+
 // OAuth flow
 
 procedure TOpenAPIWriter.WriteOAuthFlow(aObj: TOauthFlow);
@@ -1453,7 +1478,7 @@ begin
     ofkRefreshURL:
       WriteProperty(lName,aObj.RefreshURL);
     ofkScopes:
-      WriteStrings(lName,aObj.Scopes);
+      WriteStringMap(lName,aObj.Scopes);
     end;
     end;
   if aObj.HasExtensions then

+ 41 - 0
packages/fcl-openapi/tests/utOpenAPIWriter.pp

@@ -193,6 +193,14 @@ type
     procedure TestSchemaContentEncoding;
   end;
 
+  { TTestOpenApiWriterOAuth2Scopes }
+
+  TTestOpenApiWriterOAuth2Scopes = class(TTestOpenApiWriterBase)
+  Published
+    procedure TestOAuth2ScopesAsObject;
+    procedure TestOAuth2ScopesEmpty;
+  end;
+
   { TTestOpenApiWriterSecurity }
 
   TTestOpenApiWriterSecurity = class(TTestOpenApiWriterBase)
@@ -1031,6 +1039,38 @@ begin
   TestWrite('Path componentschema','{ "components" : { "schemas" : { "abc" : { "contentEncoding" : "utf8" } } } }');
 end;
 
+{ TTestOpenApiWriterOAuth2Scopes }
+
+procedure TTestOpenApiWriterOAuth2Scopes.TestOAuth2ScopesAsObject;
+var
+  Scheme: TSecuritySchemeOrReference;
+  Flow: TOAuthFlow;
+begin
+  Scheme := OpenAPI.Components.SecuritySchemes.AddItem('oauth2');
+  Scheme.Type_ := 'oauth2';
+  Flow := Scheme.Flows.ClientAuthorizationCode;
+  Flow.AuthorizationUrl := 'https://example.com/auth';
+  Flow.TokenURL := 'https://example.com/token';
+  Flow.Scopes.Add('read:user=Read user data');
+  Flow.Scopes.Add('write:user=Write user data');
+  TestWrite('OAuth2 scopes as object','{ "components" : { "securitySchemes" : { "oauth2" : { "type" : "oauth2", "flows" : { "authorizationCode" : { "authorizationUrl" : "https://example.com/auth", "tokenUrl" : "https://example.com/token", "scopes" : { "read:user" : "Read user data", "write:user" : "Write user data" } } } } } } }');
+end;
+
+procedure TTestOpenApiWriterOAuth2Scopes.TestOAuth2ScopesEmpty;
+var
+  Scheme: TSecuritySchemeOrReference;
+  Flow: TOAuthFlow;
+begin
+  Scheme := OpenAPI.Components.SecuritySchemes.AddItem('oauth2');
+  Scheme.Type_ := 'oauth2';
+  Flow := Scheme.Flows.ClientAuthorizationCode;
+  Flow.AuthorizationUrl := 'https://example.com/auth';
+  Flow.TokenURL := 'https://example.com/token';
+  // Access Scopes to ensure it's initialized but leave it empty
+  AssertEquals('Scopes count', 0, Flow.Scopes.Count);
+  TestWrite('OAuth2 empty scopes','{ "components" : { "securitySchemes" : { "oauth2" : { "type" : "oauth2", "flows" : { "authorizationCode" : { "authorizationUrl" : "https://example.com/auth", "tokenUrl" : "https://example.com/token", "scopes" : { } } } } } } }');
+end;
+
 { TTestOpenApiWriterSecurity }
 
 procedure TTestOpenApiWriterSecurity.TestOne;
@@ -1085,6 +1125,7 @@ initialization
     TTestOpenApiWriterPathResponsesLinks,
     TTestOpenApiWriterWebHooks,
     TTestOpenApiWriterComponents,
+    TTestOpenApiWriterOAuth2Scopes,
     TTestOpenApiWriterSecurity
   ]);
 

+ 32 - 0
packages/fcl-openapi/tests/utOpenApiReader.pp

@@ -146,6 +146,11 @@ Type
     Procedure TestSchemaContentEncoding;
   end;
 
+  TTestOpenApiReaderOAuth2Scopes = Class(TTestOpenAPIReader)
+    Procedure TestOAuth2ScopesAsObject;
+    Procedure TestOAuth2ScopesEmpty;
+  end;
+
   TTestOpenApiReaderSecurity = Class(TTestOpenAPIReader)
     Procedure TestOne;
   end;
@@ -1048,6 +1053,32 @@ begin
   AssertEquals('OpenAPI.components.schemas.abc.contentEncoding','utf8',OpenAPI.components.schemas['abc'].Validations.contentEncoding);
 end;
 
+{ TTestOpenApiReaderOAuth2Scopes }
+
+procedure TTestOpenApiReaderOAuth2Scopes.TestOAuth2ScopesAsObject;
+
+begin
+  TestRead('{ "components" : { "securitySchemes" : { "oauth2" : { "type" : "oauth2", "flows" : { "authorizationCode" : { "authorizationUrl" : "https://example.com/auth", "tokenUrl" : "https://example.com/token", "scopes" : { "read:user" : "Read user data", "write:user" : "Write user data" } } } } } } }');
+  AssertNotNull('OpenAPI.components',OpenAPI.components);
+  AssertNotNull('OpenAPI.components.securitySchemes',OpenAPI.components.securitySchemes);
+  AssertNotNull('OpenAPI.components.securitySchemes.oauth2',OpenAPI.components.securitySchemes['oauth2']);
+  AssertNotNull('OpenAPI.components.securitySchemes.oauth2.flows',OpenAPI.components.securitySchemes['oauth2'].flows);
+  AssertNotNull('OpenAPI.components.securitySchemes.oauth2.flows.authorizationCode',OpenAPI.components.securitySchemes['oauth2'].flows.ClientAuthorizationCode);
+  AssertEquals('scopes count', 2, OpenAPI.components.securitySchemes['oauth2'].flows.ClientAuthorizationCode.Scopes.Count);
+  AssertEquals('scope read:user value', 'Read user data', OpenAPI.components.securitySchemes['oauth2'].flows.ClientAuthorizationCode.Scopes.Values['read:user']);
+  AssertEquals('scope write:user value', 'Write user data', OpenAPI.components.securitySchemes['oauth2'].flows.ClientAuthorizationCode.Scopes.Values['write:user']);
+end;
+
+procedure TTestOpenApiReaderOAuth2Scopes.TestOAuth2ScopesEmpty;
+
+begin
+  TestRead('{ "components" : { "securitySchemes" : { "oauth2" : { "type" : "oauth2", "flows" : { "authorizationCode" : { "authorizationUrl" : "https://example.com/auth", "tokenUrl" : "https://example.com/token", "scopes" : { } } } } } } }');
+  AssertNotNull('OpenAPI.components',OpenAPI.components);
+  AssertNotNull('OpenAPI.components.securitySchemes',OpenAPI.components.securitySchemes);
+  AssertNotNull('OpenAPI.components.securitySchemes.oauth2',OpenAPI.components.securitySchemes['oauth2']);
+  AssertEquals('scopes count', 0, OpenAPI.components.securitySchemes['oauth2'].flows.ClientAuthorizationCode.Scopes.Count);
+end;
+
 
 procedure TTestOpenApiReaderSecurity.TestOne;
 
@@ -1072,6 +1103,7 @@ initialization
     TTestOpenApiReaderPathResponsesLinks,
     TTestOpenApiReaderWebHooks,
     TTestOpenApiReaderComponents,
+    TTestOpenApiReaderOAuth2Scopes,
     TTestOpenApiReaderSecurity
   ]);