Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Commit c74f87e

Browse files
authored
Merge pull request #185 from apiaryio/pksunkara/security
Add support for oauth2 in the OAS 3 parser
2 parents 3b66bde + 359d4e5 commit c74f87e

File tree

7 files changed

+469
-41
lines changed

7 files changed

+469
-41
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const R = require('ramda');
2+
const {
3+
isExtension, hasKey, getValue,
4+
} = require('../../predicates');
5+
const {
6+
createInvalidMemberWarning,
7+
} = require('../annotations');
8+
const pipeParseResult = require('../../pipeParseResult');
9+
const parseObject = require('../parseObject');
10+
const parseString = require('../parseString');
11+
12+
const name = 'Oauth Flow Object';
13+
const requiredKeys = ['scopes'];
14+
15+
/**
16+
* Parse Oauth Flow Object
17+
*
18+
* @param namespace {Namespace}
19+
* @param element {Element}
20+
* @returns ParseResult
21+
*
22+
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oauthFlowObject
23+
* @private
24+
*/
25+
function parseOauthFlowObject(context, object) {
26+
const { namespace } = context;
27+
const scopesName = `${name}' 'scopes`;
28+
29+
const parseScopeMember = R.cond([
30+
[R.T, parseString(context, scopesName, false)],
31+
]);
32+
33+
const parseScopes = pipeParseResult(namespace,
34+
parseObject(context, scopesName, parseScopeMember, [], [], true),
35+
scopes => new namespace.elements.Array(scopes.content),
36+
R.map((member) => {
37+
const scope = member.key.clone();
38+
scope.description = member.value;
39+
40+
return scope;
41+
}));
42+
43+
const parseMember = R.cond([
44+
[hasKey('scopes'), R.compose(parseScopes, getValue)],
45+
[hasKey('refreshUrl'), parseString(context, name, false)],
46+
[hasKey('authorizationUrl'), parseString(context, name, false)],
47+
[hasKey('tokenUrl'), parseString(context, name, false)],
48+
49+
// FIXME Support exposing extensions into parse result
50+
[isExtension, () => new namespace.elements.ParseResult()],
51+
52+
// Return a warning for additional properties
53+
[R.T, createInvalidMemberWarning(namespace, name)],
54+
]);
55+
56+
const parseOauthFlow = pipeParseResult(namespace,
57+
parseObject(context, name, parseMember, requiredKeys, [], true));
58+
59+
return parseOauthFlow(object);
60+
}
61+
62+
module.exports = R.curry(parseOauthFlowObject);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const R = require('ramda');
2+
const {
3+
isExtension, hasKey, getValue,
4+
} = require('../../predicates');
5+
const {
6+
createWarning,
7+
createInvalidMemberWarning,
8+
} = require('../annotations');
9+
const pipeParseResult = require('../../pipeParseResult');
10+
const parseObject = require('../parseObject');
11+
const parseOauthFlowObject = require('./parseOauthFlowObject');
12+
13+
const name = 'Oauth Flows Object';
14+
15+
const isValidFlow = R.anyPass(R.map(hasKey, ['implicit', 'password', 'clientCredentials', 'authorizationCode']));
16+
const grantTypes = {
17+
implicit: 'implicit',
18+
password: 'resource owner password credentials',
19+
clientCredentials: 'client credentials',
20+
authorizationCode: 'authorization code',
21+
};
22+
23+
/**
24+
* Parse Oauth Flows Object
25+
*
26+
* @param namespace {Namespace}
27+
* @param element {Element}
28+
* @returns ParseResult
29+
*
30+
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oauthFlowsObject
31+
* @private
32+
*/
33+
function parseOauthFlowsObject(context, object) {
34+
const { namespace } = context;
35+
36+
const parseFlow = (member) => {
37+
const key = member.key.toValue();
38+
39+
const needAuthorizationUrl = () => R.includes(key, ['implicit', 'authorizationCode']);
40+
const needTokenUrl = () => R.includes(key, ['password', 'clientCredentials', 'authorizationCode']);
41+
42+
const hasAuthorizationUrl = flow => flow.get('authorizationUrl');
43+
const hasTokenUrl = flow => flow.get('tokenUrl');
44+
45+
const parse = pipeParseResult(namespace,
46+
R.compose(parseOauthFlowObject(context), getValue),
47+
R.when(R.allPass([R.complement(hasAuthorizationUrl), needAuthorizationUrl]), () => createWarning(namespace,
48+
`'${name}' '${key}' is missing required property 'authorizationUrl'`, member)),
49+
R.when(R.allPass([R.complement(hasTokenUrl), needTokenUrl]), () => createWarning(namespace,
50+
`'${name}' '${key}' is missing required property 'tokenUrl'`, member)));
51+
52+
return parse(member);
53+
};
54+
55+
const parseMember = R.cond([
56+
[isValidFlow, parseFlow],
57+
58+
// FIXME Support exposing extensions into parse result
59+
[isExtension, () => new namespace.elements.ParseResult()],
60+
61+
// Return a warning for additional properties
62+
[R.T, createInvalidMemberWarning(namespace, name)],
63+
]);
64+
65+
const parseOauthFlows = pipeParseResult(namespace,
66+
parseObject(context, name, parseMember),
67+
flows => new namespace.elements.Array(flows.content),
68+
R.map((member) => {
69+
const authScheme = new namespace.elements.AuthScheme();
70+
71+
authScheme.element = 'Oauth2 Scheme';
72+
authScheme.push(new namespace.elements.Member('grantType', grantTypes[member.key.toValue()]));
73+
authScheme.push(member.value.getMember('scopes'));
74+
75+
return authScheme;
76+
}));
77+
78+
return parseOauthFlows(object);
79+
}
80+
81+
module.exports = R.curry(parseOauthFlowsObject);

packages/fury-adapter-oas3-parser/lib/parser/oas/parseSecuritySchemeObject.js

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,18 @@ const parseString = require('../parseString');
1313

1414
const name = 'Security Scheme Object';
1515
const requiredKeys = ['type'];
16-
const unsupportedKeys = [
17-
'bearerFormat', 'flows', 'openIdConnectUrl',
18-
];
16+
const unsupportedKeys = ['bearerFormat', 'openIdConnectUrl'];
1917
const isUnsupportedKey = R.anyPass(R.map(hasKey, unsupportedKeys));
20-
const passThrough = R.anyPass(R.map(hasKey, ['name', 'in', 'scheme']));
18+
const passThrough = R.anyPass(R.map(hasKey, ['name', 'in', 'scheme', 'flows']));
2119

2220
const isApiKeyScheme = securityScheme => securityScheme.getValue('type') === 'apiKey';
2321
const isHttpScheme = securityScheme => securityScheme.getValue('type') === 'http';
22+
// const isOauth2Scheme = securityScheme => securityScheme.getValue('type') === 'oauth2';
2423

25-
const isValidTypeValue = R.anyPass([
26-
hasValue('apiKey'), hasValue('http'), hasValue('oauth2'), hasValue('openIdConnect'),
27-
]);
28-
const isSupportedType = R.anyPass([
29-
hasValue('apiKey'), hasValue('http'),
30-
]);
31-
const isValidInValue = R.anyPass([
32-
hasValue('query'), hasValue('header'), hasValue('cookie'),
33-
]);
34-
const isSupportedIn = R.anyPass([
35-
hasValue('query'), hasValue('header'),
36-
]);
24+
const isValidTypeValue = R.anyPass(R.map(hasValue, ['apiKey', 'http', 'oauth2', 'openIdConnect']));
25+
const isSupportedType = R.anyPass(R.map(hasValue, ['apiKey', 'http', 'oauth2']));
26+
const isValidInValue = R.anyPass(R.map(hasValue, ['query', 'header', 'cookie']));
27+
const isSupportedIn = R.anyPass(R.map(hasValue, ['query', 'header']));
3728

3829
function validateApiKeyScheme(context, securityScheme) {
3930
const { namespace } = context;
@@ -119,6 +110,7 @@ function parseSecuritySchemeObject(context, object) {
119110
parseObject(context, name, parseMember, requiredKeys, [], true),
120111
R.when(isApiKeyScheme, R.curry(validateApiKeyScheme)(context)),
121112
R.when(isHttpScheme, R.curry(validateHttpScheme)(context)),
113+
// R.when(isOauth2Scheme, parseSecuritySchemeFlowsObject),
122114
(securityScheme) => {
123115
const authScheme = new namespace.elements.AuthScheme();
124116

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
const { Fury } = require('fury');
2+
const { expect } = require('../../chai');
3+
const parse = require('../../../../lib/parser/oas/parseOauthFlowObject');
4+
const Context = require('../../../../lib/context');
5+
6+
const { minim: namespace } = new Fury();
7+
8+
describe('Oauth Flow Object', () => {
9+
let context;
10+
beforeEach(() => {
11+
context = new Context(namespace);
12+
});
13+
14+
it('provides warning when oauth flow is not an object', () => {
15+
const oauthFlow = new namespace.elements.String();
16+
17+
const parseResult = parse(context, oauthFlow);
18+
19+
expect(parseResult.length).to.equal(1);
20+
expect(parseResult).to.contain.warning("'Oauth Flow Object' is not an object");
21+
});
22+
23+
describe('#scopes', () => {
24+
it('provides a warning when scopes does not exist', () => {
25+
const oauthFlow = new namespace.elements.Object({
26+
});
27+
28+
const parseResult = parse(context, oauthFlow);
29+
30+
expect(parseResult.length).to.equal(1);
31+
expect(parseResult).to.contain.warning("'Oauth Flow Object' is missing required property 'scopes'");
32+
});
33+
34+
it('provides a warning when scopes is not an object', () => {
35+
const oauthFlow = new namespace.elements.Object({
36+
scopes: 1,
37+
});
38+
39+
const parseResult = parse(context, oauthFlow);
40+
41+
expect(parseResult.length).to.equal(1);
42+
expect(parseResult).to.contain.warning("'Oauth Flow Object' 'scopes' is not an object");
43+
});
44+
45+
it('provides a warning when scopes value item is not a string', () => {
46+
const oauthFlow = new namespace.elements.Object({
47+
scopes: {
48+
read: 1,
49+
},
50+
});
51+
52+
const parseResult = parse(context, oauthFlow);
53+
54+
expect(parseResult.length).to.equal(2);
55+
expect(parseResult).to.contain.warning("'Oauth Flow Object' 'scopes' 'read' is not a string");
56+
57+
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.Object);
58+
expect(parseResult.get(0).get('scopes')).to.be.instanceof(namespace.elements.Array);
59+
expect(parseResult.get(0).get('scopes').length).to.equal(0);
60+
});
61+
62+
it('parses scopes correctly', () => {
63+
const oauthFlow = new namespace.elements.Object({
64+
scopes: {
65+
read: 'description',
66+
},
67+
});
68+
69+
const parseResult = parse(context, oauthFlow);
70+
71+
expect(parseResult.length).to.equal(1);
72+
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.Object);
73+
74+
const scopes = parseResult.get(0).get('scopes');
75+
76+
expect(scopes).to.be.instanceof(namespace.elements.Array);
77+
expect(scopes.length).to.equal(1);
78+
79+
expect(scopes.get(0)).to.be.instanceof(namespace.elements.String);
80+
expect(scopes.get(0).toValue()).to.equal('read');
81+
expect(scopes.get(0).description).to.not.be.undefined;
82+
expect(scopes.get(0).description.toValue()).to.equal('description');
83+
});
84+
});
85+
86+
describe('#refreshUrl', () => {
87+
it('provides an warning when refreshUrl is not a string', () => {
88+
const oauthFlow = new namespace.elements.Object({
89+
scopes: {},
90+
refreshUrl: 1,
91+
});
92+
93+
const parseResult = parse(context, oauthFlow);
94+
95+
expect(parseResult.length).to.equal(2);
96+
expect(parseResult).to.contain.warning("'Oauth Flow Object' 'refreshUrl' is not a string");
97+
98+
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.Object);
99+
expect(parseResult.get(0).get('scopes')).to.be.instanceof(namespace.elements.Array);
100+
expect(parseResult.get(0).get('scopes').length).to.equal(0);
101+
});
102+
});
103+
104+
describe('#tokenUrl', () => {
105+
it('provides an warning when tokenUrl is not a string', () => {
106+
const oauthFlow = new namespace.elements.Object({
107+
scopes: {},
108+
tokenUrl: 1,
109+
});
110+
111+
const parseResult = parse(context, oauthFlow);
112+
113+
expect(parseResult.length).to.equal(2);
114+
expect(parseResult).to.contain.warning("'Oauth Flow Object' 'tokenUrl' is not a string");
115+
116+
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.Object);
117+
expect(parseResult.get(0).get('scopes')).to.be.instanceof(namespace.elements.Array);
118+
expect(parseResult.get(0).get('scopes').length).to.equal(0);
119+
});
120+
});
121+
122+
describe('#authorizationUrl', () => {
123+
it('provides an warning when authorizationUrl is not a string', () => {
124+
const oauthFlow = new namespace.elements.Object({
125+
scopes: {},
126+
authorizationUrl: 1,
127+
});
128+
129+
const parseResult = parse(context, oauthFlow);
130+
131+
expect(parseResult.length).to.equal(2);
132+
expect(parseResult).to.contain.warning("'Oauth Flow Object' 'authorizationUrl' is not a string");
133+
134+
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.Object);
135+
expect(parseResult.get(0).get('scopes')).to.be.instanceof(namespace.elements.Array);
136+
expect(parseResult.get(0).get('scopes').length).to.equal(0);
137+
});
138+
});
139+
140+
it('provides warning for invalid keys', () => {
141+
const oauthFlow = new namespace.elements.Object({
142+
scopes: {},
143+
invalid: '',
144+
});
145+
146+
const parseResult = parse(context, oauthFlow);
147+
148+
expect(parseResult.length).to.equal(2);
149+
expect(parseResult).to.contain.warning("'Oauth Flow Object' contains invalid key 'invalid'");
150+
});
151+
152+
it('does not provide warning/errors for extensions', () => {
153+
const oauthFlow = new namespace.elements.Object({
154+
scopes: {},
155+
'x-extension': '',
156+
});
157+
158+
const parseResult = parse(context, oauthFlow);
159+
160+
expect(parseResult).to.not.contain.annotations;
161+
});
162+
});

0 commit comments

Comments
 (0)