|
|
@@ -1,3 +1,4 @@
|
|
|
+#include <algorithm>
|
|
|
#include <cctype>
|
|
|
#include <unordered_set>
|
|
|
#include "credential_controller.hpp"
|
|
|
@@ -50,7 +51,123 @@ std::unordered_map<std::string, int> CredentialController::credentialUsage() {
|
|
|
return usage;
|
|
|
}
|
|
|
|
|
|
+bool CredentialController::mayUse(const auth::AuthContext& ctx,
|
|
|
+ const credentials::CredentialInfo& cred) const {
|
|
|
+ if (access_.allowed(ctx, cred.project_id, auth::Action::Read)) return true;
|
|
|
+ for (const auto& user_id : cred.shared_with) {
|
|
|
+ if (user_id == ctx.user_id) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+bool CredentialController::mayManage(const auth::AuthContext& ctx,
|
|
|
+ const credentials::CredentialInfo& cred) const {
|
|
|
+ // Being shared with never carries the right to change it, delete it or pass
|
|
|
+ // it on. Otherwise sharing would quietly hand over the credential itself.
|
|
|
+ return access_.allowed(ctx, cred.project_id, auth::Action::Write);
|
|
|
+}
|
|
|
+
|
|
|
+void CredentialController::shareCredential(const httplib::Request& req, httplib::Response& res,
|
|
|
+ const auth::AuthContext& ctx) {
|
|
|
+ const std::string id = req.matches[1].str();
|
|
|
+ auto existing = credential_store_.get(id);
|
|
|
+ if (existing.failed()) {
|
|
|
+ sendError(res, "Credential not found", 404);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 404 rather than 403 for somebody who cannot see it at all: whether a
|
|
|
+ // credential exists is only told to people who can reach it.
|
|
|
+ if (!mayUse(ctx, existing.value())) {
|
|
|
+ sendError(res, "Credential not found", 404);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!mayManage(ctx, existing.value())) {
|
|
|
+ sendError(res, "Only someone who can change this credential can share it", 403);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ nlohmann::json body;
|
|
|
+ try {
|
|
|
+ body = nlohmann::json::parse(req.body);
|
|
|
+ } catch (...) {
|
|
|
+ sendError(res, "Invalid JSON body", 400);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const std::string user_id = body.value("userId", "");
|
|
|
+ if (user_id.empty()) {
|
|
|
+ sendError(res, "Say which user to share it with, as userId", 400);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (user_id == ctx.user_id) {
|
|
|
+ sendError(res, "You already reach this credential - sharing it with yourself "
|
|
|
+ "would do nothing", 400);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto shared = existing.value().shared_with;
|
|
|
+ if (std::find(shared.begin(), shared.end(), user_id) == shared.end()) {
|
|
|
+ shared.push_back(user_id);
|
|
|
+ }
|
|
|
+ auto updated = credential_store_.setSharedWith(id, shared);
|
|
|
+ if (updated.failed()) {
|
|
|
+ sendError(res, updated.error().message(), 500);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ LOG_INFO("Credential {} shared with user {} by {}", id, user_id, ctx.user_id);
|
|
|
+ sendJson(res, updated.value().toJson());
|
|
|
+}
|
|
|
+
|
|
|
+void CredentialController::unshareCredential(const httplib::Request& req, httplib::Response& res,
|
|
|
+ const auth::AuthContext& ctx) {
|
|
|
+ const std::string id = req.matches[1].str();
|
|
|
+ const std::string user_id = req.matches[2].str();
|
|
|
+
|
|
|
+ auto existing = credential_store_.get(id);
|
|
|
+ if (existing.failed()) {
|
|
|
+ sendError(res, "Credential not found", 404);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!mayUse(ctx, existing.value())) {
|
|
|
+ sendError(res, "Credential not found", 404);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Somebody may always give up their own access, which needs no rights over
|
|
|
+ // the credential at all.
|
|
|
+ if (user_id != ctx.user_id && !mayManage(ctx, existing.value())) {
|
|
|
+ sendError(res, "Only someone who can change this credential can take a share away", 403);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto shared = existing.value().shared_with;
|
|
|
+ shared.erase(std::remove(shared.begin(), shared.end(), user_id), shared.end());
|
|
|
+ auto updated = credential_store_.setSharedWith(id, shared);
|
|
|
+ if (updated.failed()) {
|
|
|
+ sendError(res, updated.error().message(), 500);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ LOG_INFO("Credential {} unshared from user {} by {}", id, user_id, ctx.user_id);
|
|
|
+ sendJson(res, updated.value().toJson());
|
|
|
+}
|
|
|
+
|
|
|
void CredentialController::registerRoutes(httplib::Server& server) {
|
|
|
+ server.Post(R"(/api/v1/credentials/([^/]+)/shares)",
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
+ middleware_.requireAuth(req, res, [this](const httplib::Request& req,
|
|
|
+ httplib::Response& res,
|
|
|
+ const auth::AuthContext& ctx) {
|
|
|
+ shareCredential(req, res, ctx);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ server.Delete(R"(/api/v1/credentials/([^/]+)/shares/([^/]+))",
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
+ middleware_.requireAuth(req, res, [this](const httplib::Request& req,
|
|
|
+ httplib::Response& res,
|
|
|
+ const auth::AuthContext& ctx) {
|
|
|
+ unshareCredential(req, res, ctx);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
// List credentials - requires admin role
|
|
|
server.Get("/api/v1/credentials", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
middleware_.requireAuth(req, res, [this](const httplib::Request& req,
|
|
|
@@ -122,7 +239,7 @@ void CredentialController::listCredentials(const httplib::Request& req, httplib:
|
|
|
const auto usage = credentialUsage();
|
|
|
nlohmann::json credentials_json = nlohmann::json::array();
|
|
|
for (const auto& cred : result.value()) {
|
|
|
- if (!reachable.contains(cred.project_id)) continue;
|
|
|
+ if (!reachable.contains(cred.project_id) && !mayUse(ctx, cred)) continue;
|
|
|
auto entry = cred.toJson();
|
|
|
const auto found = usage.find(cred.id);
|
|
|
entry["usedByWorkflows"] = found == usage.end() ? 0 : found->second;
|
|
|
@@ -142,7 +259,7 @@ void CredentialController::getCredential(const httplib::Request& req, httplib::R
|
|
|
sendError(res, result.error().message(), status);
|
|
|
return;
|
|
|
}
|
|
|
- if (!access_.allowed(ctx, result.value().project_id, auth::Action::Read)) {
|
|
|
+ if (!mayUse(ctx, result.value())) {
|
|
|
sendError(res, "Credential not found", 404);
|
|
|
return;
|
|
|
}
|
|
|
@@ -206,11 +323,14 @@ void CredentialController::updateCredential(const httplib::Request& req, httplib
|
|
|
sendError(res, "Credential not found", 404);
|
|
|
return;
|
|
|
}
|
|
|
- if (!access_.allowed(ctx, existing.value().project_id, auth::Action::Read)) {
|
|
|
+ // Somebody it was shared with can see it, so "not found" would be a lie
|
|
|
+ // to them - they get the real reason instead. To anybody who cannot see
|
|
|
+ // it at all, whether it exists is still not something to disclose.
|
|
|
+ if (!mayUse(ctx, existing.value())) {
|
|
|
sendError(res, "Credential not found", 404);
|
|
|
return;
|
|
|
}
|
|
|
- if (!access_.allowed(ctx, existing.value().project_id, auth::Action::Write)) {
|
|
|
+ if (!mayManage(ctx, existing.value())) {
|
|
|
sendError(res, "You can see this credential but not change it", 403);
|
|
|
return;
|
|
|
}
|
|
|
@@ -254,11 +374,14 @@ void CredentialController::deleteCredential(const httplib::Request& req, httplib
|
|
|
sendError(res, "Credential not found", 404);
|
|
|
return;
|
|
|
}
|
|
|
- if (!access_.allowed(ctx, existing.value().project_id, auth::Action::Read)) {
|
|
|
+ // Somebody it was shared with can see it, so "not found" would be a lie
|
|
|
+ // to them - they get the real reason instead. To anybody who cannot see
|
|
|
+ // it at all, whether it exists is still not something to disclose.
|
|
|
+ if (!mayUse(ctx, existing.value())) {
|
|
|
sendError(res, "Credential not found", 404);
|
|
|
return;
|
|
|
}
|
|
|
- if (!access_.allowed(ctx, existing.value().project_id, auth::Action::Write)) {
|
|
|
+ if (!mayManage(ctx, existing.value())) {
|
|
|
sendError(res, "You can see this credential but not delete it", 403);
|
|
|
return;
|
|
|
}
|