Skip to content

Commit

Permalink
fix #1675
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuancelin committed Jul 18, 2023
1 parent a0c6274 commit 39dd249
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 9 deletions.
84 changes: 82 additions & 2 deletions otoroshi/app/controllers/BackOfficeController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import otoroshi.jobs.newengine.NewEngine
import otoroshi.jobs.updates.SoftwareUpdatesJobs
import otoroshi.models.RightsChecker.SuperAdminOnly
import otoroshi.models._
import otoroshi.next.models.{GraphQLFormats, NgRoute, NgRouteComposition}
import otoroshi.next.models.{GraphQLFormats, NgRoute, NgRouteComposition, NgTarget}
import otoroshi.next.plugins.EurekaServerSink
import otoroshi.next.plugins.api.NgPluginHelper
import otoroshi.next.proxy.{BackOfficeRequest, ProxyEngine}
Expand Down Expand Up @@ -1543,7 +1543,6 @@ class BackOfficeController(
def createCsr =
BackOfficeActionAuth.async(parse.json) { ctx =>
val issuerRef = (ctx.request.body \ "caRef").asOpt[String]

GenCsrQuery.fromJson(ctx.request.body) match {
case Left(err) => BadRequest(Json.obj("error" -> err)).future
case Right(query) => {
Expand Down Expand Up @@ -2034,4 +2033,85 @@ class BackOfficeController(
}
}
}

def testFilteringAndProjection() = BackOfficeActionAuth(parse.json) { ctx =>

val body = ctx.request.body
val input = body.select("input").asOpt[JsValue].getOrElse(Json.obj())
val matchExpressions: JsObject = body.select("match").asOpt[JsObject].getOrElse(Json.obj())
val matchIncludeExpressions: Seq[JsObject] = matchExpressions.select("include").asOpt[Seq[JsObject]].getOrElse(Seq.empty[JsObject])
val matchExcludeExpressions: Seq[JsObject] = matchExpressions.select("exclude").asOpt[Seq[JsObject]].getOrElse(Seq.empty[JsObject])
val projectionExpression: JsObject = body.select("projection").asOpt[JsObject].getOrElse(Json.obj())

val shouldInclude = if (matchIncludeExpressions.isEmpty) true else matchIncludeExpressions.forall(expr => otoroshi.utils.Match.matches(input, expr))
val shouldExclude = if (matchExcludeExpressions.isEmpty) false else matchExcludeExpressions.forall(expr => otoroshi.utils.Match.matches(input, expr))

val matches = shouldInclude && !shouldExclude

val projected = otoroshi.utils.Projection.project(input, projectionExpression, identity)

Ok(Json.obj("matches" -> matches, "projection" -> projected))
}

def testFilteringAndProjectionInputDoc() = BackOfficeActionAuth { ctx =>
val rawRequest = ctx.request
val route = NgRoute.empty
val target = NgTarget.default
Ok(GatewayEvent(
`@id` = env.snowflakeGenerator.nextIdStr(),
reqId = env.snowflakeGenerator.nextIdStr(),
parentReqId = None,
`@timestamp` = DateTime.now(),
`@calledAt` = DateTime.now(),
protocol = ctx.request.theProtocol,
to = Location(
scheme = rawRequest.theProtocol,
host = rawRequest.theHost,
uri = rawRequest.relativeUri
),
target = Location(
scheme = target.toTarget.scheme,
host = target.toTarget.host,
uri = rawRequest.relativeUri
),
duration = 30L,
overhead = 10L,
cbDuration = 0L,
overheadWoCb = 10L,
callAttempts = 1,
url = rawRequest.theUrl,
method = rawRequest.method,
from = rawRequest.theIpAddress,
env = "prod",
data = DataInOut(
dataIn = 0L,
dataOut = 128L
),
status = 200,
headers = rawRequest.headers.toSimpleMap.toSeq.map(Header.apply),
headersOut = Seq.empty,
otoroshiHeadersIn = rawRequest.headers.toSimpleMap.toSeq.map(Header.apply),
otoroshiHeadersOut = Seq.empty,
extraInfos = None,
identity = Identity(
identityType = "APIKEY",
identity = "client_id",
label = "client"
).some,
responseChunked = false,
`@serviceId` = s"route_${IdGenerator.uuid}",
`@service` = route.name,
descriptor = Some(route.legacy),
route = Some(route),
`@product` = route.metadata.getOrElse("product", "--"),
remainingQuotas = RemainingQuotas(),
viz = None,
clientCertChain = rawRequest.clientCertChainPem,
err = false,
gwError = None,
userAgentInfo = None,
geolocationInfo = None,
extraAnalyticsData = None
).toJson)
}
}
2 changes: 1 addition & 1 deletion otoroshi/app/next/controllers/tryit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class TryItController(
Accumulator.source[ByteString].map(Right.apply)
}

def dataExporterCall() = BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
def kafkaDataExporterTryIt() = BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
val jsonBody = bodyRaw.utf8String.parseJson
jsonBody \ "config" match {
Expand Down
11 changes: 11 additions & 0 deletions otoroshi/app/next/models/backend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,17 @@ case class NgTarget(
}

object NgTarget {
val default = NgTarget(
id = "www.otoroshi.io",
hostname = "www.otoroshi.io",
port = 443,
tls = true,
weight = 1,
protocol = HttpProtocols.HTTP_1_1,
predicate = AlwaysMatch,
ipAddress = None,
tlsConfig = NgTlsConfig.default
)
def fromLegacy(target: Target): NgTarget = fromTarget(target)
def fromTarget(target: Target): NgTarget = {
NgTarget(
Expand Down
4 changes: 3 additions & 1 deletion otoroshi/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ PUT /bo/api/backoffice/flags otoroshi.controlle
GET /bo/api/backoffice/flags otoroshi.controllers.BackOfficeController.getFlags()

POST /bo/api/tryit otoroshi.next.controllers.TryItController.call(entity: Option[String])
POST /bo/api/data-exporter/tryit otoroshi.next.controllers.TryItController.dataExporterCall()
POST /bo/api/data-exporter/kafkatryit otoroshi.next.controllers.TryItController.kafkaDataExporterTryIt()

GET /bo/api/graphqlproxy otoroshi.controllers.BackOfficeController.graphqlProxy()
POST /bo/api/graphqlproxy otoroshi.controllers.BackOfficeController.graphqlProxy()
Expand All @@ -103,6 +103,8 @@ POST /bo/api/graphql_to_json otoroshi.controlle
POST /bo/api/json_to_graphql_schema otoroshi.controllers.BackOfficeController.jsonToGraphqlSchema()
POST /bo/api/json_to_yaml otoroshi.controllers.BackOfficeController.toYaml()
GET /bo/api/plugins/wasm otoroshi.controllers.BackOfficeController.wasmFiles()
GET /bo/api/test_match_and_project_input otoroshi.controllers.BackOfficeController.testFilteringAndProjectionInputDoc()
POST /bo/api/test_match_and_project otoroshi.controllers.BackOfficeController.testFilteringAndProjection()

# Admin API proxy
GET /bo/api/proxy/*path otoroshi.controllers.BackOfficeController.proxyAdminApi(path)
Expand Down
105 changes: 102 additions & 3 deletions otoroshi/javascript/src/pages/DataExportersPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ export class DataExportersPage extends Component {
}
}

const ExporterTryIt = ({ exporter }) => {
const KafkaExporterTryIt = ({ exporter }) => {
const [status, setStatus] = useState('Not tested');
const [timeout, setConnectionTimeout] = useState(15);

Expand Down Expand Up @@ -501,7 +501,7 @@ const ExporterTryIt = ({ exporter }) => {
icon={() => <i className="fas fa-hammer" />}
onPress={() => {
setStatus('Processing ...');
return BackOfficeServices.dataExportertryIt({ ...exporter, timeout }).then(
return BackOfficeServices.kafkaDataExportertryIt({ ...exporter, timeout }).then(
(res) => {
if (res.status === 200) setStatus('Successful');
else res.json().then((err) => setStatus(err.error));
Expand Down Expand Up @@ -537,6 +537,16 @@ export class NewExporterForm extends Component {
this.props.onChange({ ...this.props.value, ...obj });
};

testMatchAndProject = () => {
window.popup(
"Test filtering and projection expressions",
(ok, cancel) => (
<TestMatchAndProjectModal ok={ok} cancel={cancel} originalFiltering={this.data().filtering} originalProjection={this.data().projection} />
),
{ additionalClass: 'modal-dialog modal-xl' }
)
}

render() {
return (
<>
Expand Down Expand Up @@ -596,6 +606,12 @@ export class NewExporterForm extends Component {
onChange={(e) => this.dataChange({ projection: e })}
height="200px"
/>
<div className="row mb-3">
<label className="col-sm-2"></label>
<div className="col-sm-10">
<button type="button" className="btn btn-primary" onClick={this.testMatchAndProject}><i className="fas fa-vial" /> Test filtering and projection expressions</button>
</div>
</div>
</Collapse>
<Collapse initCollapsed={true} label="Queue details">
<NumberInput
Expand Down Expand Up @@ -652,7 +668,7 @@ export class NewExporterForm extends Component {
/>
</Collapse>
)}
{this.data().type === 'kafka' && <ExporterTryIt exporter={this.props.value} />}
{this.data().type === 'kafka' && <KafkaExporterTryIt exporter={this.props.value} />}
</form>
</>
);
Expand Down Expand Up @@ -1642,3 +1658,86 @@ const possibleExporterConfigFormValues = {
},
},
};

class TestMatchAndProjectModal extends Component {

state = { input: {}, filtering: this.props.originalFiltering, projection: this.props.originalProjection, output: { message: 'no test yet' } }

componentDidMount() {
fetch('/bo/api/test_match_and_project_input', {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
},
}).then(r => r.json()).then(r => {
this.setState({ input: r })
})
}

test = () => {
this.setState({ output: "running test ..." }, () => {
fetch('/bo/api/test_match_and_project', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
input: this.state.input,
match: this.state.filtering,
projection: this.state.projection,
})
}).then(r => r.json()).then(r => {
if (r.matches) {
this.setState({ output: r.projection })
} else {
this.setState({ output: { message: 'The input document does not match the filtering expressions' }})
}
});
});
}

render() {
return (
<>
<div className="modal-body">
<div style={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '50%' }}>
<JsonObjectAsCodeInput
label="Input"
value={this.state.input}
onChange={(e) => this.setState({ input: e })}
/>
<JsonObjectAsCodeInput
label="Output"
value={this.state.output}
onChange={(e) => ('')}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', width: '50%' }}>
<JsonObjectAsCodeInput
label="Filtering"
value={this.state.filtering}
onChange={(e) => this.setState({ filtering: e })}
/>
<JsonObjectAsCodeInput
label="Projection"
value={this.state.projection}
onChange={(e) => this.setState({ projection: e })}
/>
</div>
</div>
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<button type="button" className="btn btn-success" onClick={this.test}><i className="fas fa-play" /> run</button>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-danger" onClick={this.props.cancel}>
close
</button>
</div>
</>
);
}
}
4 changes: 2 additions & 2 deletions otoroshi/javascript/src/services/BackOfficeServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -2015,8 +2015,8 @@ export function tryIt(content, entity) {
});
}

export function dataExportertryIt(content) {
return fetch('/bo/api/data-exporter/tryit', {
export function kafkaDataExportertryIt(content) {
return fetch('/bo/api/data-exporter/kafkatryit', {
method: 'POST',
credentials: 'include',
headers: {
Expand Down

0 comments on commit 39dd249

Please sign in to comment.