poniedziałek, 23 stycznia 2023

Użycie Sqlite w aplikacji ASP.NET Core API wraz z Entity Framework Core

 W tym poście pokaże, jak łatwo podpiąć się pod bazę Sqlite z projektu ASP.NET Core API. Sqlite nie wymaga instalowania oprogramowania serwera baz danych. Więcej na temat tego kiedy używać Sqlite można przeczytać na oficjalnej stronie.

1. Pierwszym krokiem aby użyć Sqlite wraz z Entity Framework Core jest instalacja odpowiednich nugetów (paczek). Najłatwiej to zrobić za pomocą managera Nuget dostępnego w Visual Studio. Klikamy prawym klawiszem myszy na projekt i następnie wybieramy opcję Manage NuGet Packages...:


Instalujemy następujące paczki:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Sqlite
  • Microsoft.EntityFrameworkCore.Tools


2. Następny krokiem jest dodanie ConnectionString do ustawień aplikacji. W pliku appsettings.json dodajemy wpis:

  "ConnectionStrings": {
    "DatabaseConnection": "Data Source=books.db"
  }

Pierwszy element to nazwa połączenia, drugi to właściwy ConnectionString. Plik konfiguracyjny będzie wyglądać tak po tej zmianie:


3. Jako, że mamy podejście Code First (czyli najpierw piszemy kod a na jego podstawie generujemy bazę danych) - tworzymy klasę modelu. Przykładowo klasa Book:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BookStoreApi.Models;

public class Book
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    public string Author { get; set; }

    public int PublishYear { get; set; }

    public DateTime CreateDate { get; set; }
}

Na pole Id oraz Name nałożone są dodatkowe atrybuty:

  • Key - identyfikuje pole, które będzie kluczem głównym dla tabeli
  • DatabaseGeneratedOption.Identity - podczas tworzenia wiersza w bazie, baza danych automatycznie wygeneruje jego wartość (w tym przypadku klucz jest typu int więc będzie to kolejna wartość sekwencji)
  • Required - identyfikuje kolumnę, która musi zostać wypełniona aby wiersz został dodany do tabeli

Możliwych atrybutów oczywiście jest znacznie więcej. Dla potrzeby tego przykładu nie będzie użytych więcej. 


4. Tworzymy klasę odpowiedzialną za połączenie z bazą danych, tzw. DbCotext

using BookStoreApi.Models;
using Microsoft.EntityFrameworkCore;

namespace BookStoreApi.Data;

public class BookStoreDbContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>().HasData(
            new()
            {
                Id = 1,
                Name =
                    "C#: 3 books in 1 - The Ultimate Beginner, Intermediate & Advanced Guides to Master C# Programming Quickly with No Experience",
                Author = "Mark Reed",
                PublishYear = 2022,
                CreateDate = new DateTime(2023, 1, 1)
            },
            new()
            {
                Id = 2, Name = "C++ For Dummies 7th Edition", Author = "Stephen R. Davis", PublishYear = 2014,
                CreateDate = new DateTime(2023, 1, 2)
            },
            new()
            {
                Id = 3,
                Name = "How to Make a Video Game All By Yourself: 10 steps, just you and a computer",
                Author = "Matt Hackett",
                PublishYear = 2022,
                CreateDate = new DateTime(2023, 1, 3)
            });
    }
}

Metoda OnModelCreating nie jest obowiązkowa w implementacji. Pozwala zainicjować początkowe dane w bazie, co może nam się przydać do testów, jak i również możemy za jej pomocą wprowadzić dane słownikowe.  


5. Kolejnym etapem jest rejestracja DBContext w kontenerze DI (Dependency Injection). Rejestracji dokonujemy w pliku Program.cs:

// Add services to the container.

builder.Services.AddDbContext<BookStoreDbContext>(option =>
    option.UseSqlite(builder.Configuration.GetConnectionString("DatabaseConnection")));


6. Stworzymy teraz przykładową akcję pobierająca książki z bazy danych. 

[ApiController]
[Route("api/[controller]")]
public class BookStoreController : ControllerBase
{
    private readonly BookStoreDbContext _bookStoreDbContext;

    public BookStoreController(BookStoreDbContext bookStoreDbContext)
    {
        _bookStoreDbContext = bookStoreDbContext;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
    {
        return Ok(await _bookStoreDbContext.Books.ToListAsync());
    }
}


7. Od strony kodu, mamy już wszystko co jest potrzebne. Kolejne kroki stworzą naszą bazę danych. Otwieramy Package Manager Console (View -> Other Windows -> Package Manager Console) 



8. W konsoli wprowadzamy komendę add-migration migration_name przykładowo:

add-migration CreateBookDatabase


Jeżeli komenda zakończy się sukcesem powinniśmy zobaczyć nowy katalog w naszym projekcie nazwany Migrations


9. Teraz przejdziemy do właściwego utworzenia bazy danych. Po stworzeniu migracji, musimy zaaplikować zmiany do bazy danych (niezależnie czy jest to Sqlite czy inny silnik bazodanowy). Służy do tego komenda update-database:



10. Wszystko! Możemy uruchomić projekt i zobaczyć w akcji jak działa połączenie z naszą bazą danych:



poniedziałek, 9 stycznia 2023

Partial Update - PATCH w ASP.NET Core API

Partial Update - czyli częściowa aktualizacja zasobów możliwa jest przy użyciu metody PATCH. Jak powinniśmy używać PATCH opisane jest na stronie JSON Patch | jsonpatch.com Polecam zwłaszcza zobaczyć na opis składni, która nie jest od razu oczywista. Sama operacja partial update jest prosta i bardzo elastyczna a większość logiki załatwia za nas odpowiednia biblioteka. 

Aby w łatwy sposób skorzystać z PATCH w projekcie:

1. Instalujemy paczkę nuget Microsoft.AspNetCore.Mvc.NewtonsoftJson:


2. Zmieniamy bibliotekę formatującą JSONa z System.Text.Json na NewtonsoftJson:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers()
    .AddNewtonsoftJson();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

3. Tworzymy akcję w kontrolerze:

    [HttpPatch("{id:int}")]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public IActionResult UpdatePartialBook(int id, JsonPatchDocument<BookDto> bookPatchObject)
    {
        if (bookPatchObject == null || id == 0)
        {
            return BadRequest();
        }

        var book = //Code to get book from DB (or any other data source)
        if (book == null)
        {
            return NotFound();
        }

        bookPatchObject.ApplyTo(book, ModelState);
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return NoContent();
    }


I właściwie tyle :)


Teraz możemy przetestować naszą metodę w akcji. Przykładowo zmienimy autora dla pierwszej książki:

Przed zmianą:


Wywołujemy partial update:


Po zmianie potwierdzamy, że aktualizacja się powiodła:

wtorek, 3 stycznia 2023

Zwrócenie lokalizacji do zasobu po jego stworzeniu - CreatedAtRoute

Zasoby w WEB API ASP.NET Core tworzy się za pomocą akcji POST. Przykładowy kod tworzący zasób (w tym przypadku książkę):

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookDto bookDto)
    {
        if (bookDto == null)
        {
            return BadRequest(bookDto);
        }

        if (bookDto.Id > 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError);
        }

        //Code to store book in data storage...

        return Ok(bookDto);
    }

Powyższy kod jest jak najbardziej poprawny i stworzy żądany zasób:


Czasami jednak potrzebujemy zwrócić lokalizację (link) do utworzonego zasobu. Z pomocą przychodzi metoda CreatedAtRoute. Pierwszym parametrem tej metody jest akcja którą chcemy wywołać. W tym przypadku zakładamy, że chcemy odesłać użytkownika do metody pozwalającej pobrać książkę po Id. HttpGet oprócz parametrów wejściowych pozawala nazwać akcję co następnie pozwoli jej użyć przy przekierowaniu. Tak więc nasza metoda GetBook powinna wyglądać następująco:

    [HttpGet("{id:int}", Name = nameof(GetBook))]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<BookDto>> GetBook(int id)
    {
        if (id <= 0)
        {
            return BadRequest();
        }
        //code...
     }
Następnie w metodzie HttpPost dokonujemy dwóch aktualizacji (obsługa HTTP Status Code 201 Created oraz wywołanie metody CreatedAtRoute):

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookDto bookDto)
    {
        if (bookDto == null)
        {
            return BadRequest(bookDto);
        }

        if (bookDto.Id > 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError);
        }

        //Code to store book in data storage...

        return CreatedAtRoute(nameof(GetBook), new { id = bookDto.Id }, bookDto);
    }


Po uruchomieniu i przetestowaniu kodu w Headerach otrzymamy link do lokalizacji nowo utworzonego zasobu:


Otwierając ten link w przeglądarce możemy potwierdzić, że zasób został poprawnie stworzony:



poniedziałek, 2 stycznia 2023

Swagger Undocumented Status Code - dokumentacja kodów zwracanych przez akcje Controllera

Poniższy kawałek kodu obrazuje prostą metodę zwracającą pojedynczą książkę na podstawie jej identyfikatora. Jeżeli identyfikator jest mniejszy lub równy zero zwracamy błąd Http Status Code 400 (zakładamy w tym przypadku, że w naszej bazie danych książki posiadają identyfikatory od 1 w górę).

    [HttpGet("{id:int}")]
    public async Task<ActionResult<BookDto>> GetBook(int id)
    {
        if (id <= 0)
        {
            return BadRequest();
        }
        //... Code, code...
     }
Kod jest jak najbardziej poprawy. Co może nas zdziwić, to że uruchamiając aplikację i testując ją pojawi nam się dziwny komunikat o braku dokumentacji dla kodu 400:



Powyższy brak można rozwiązać za pomocą atrybutu ProducesResponseType. Za jego pomocą definiujemy możliwe rezultaty z naszej metody jak i możemy jawnie wskazać scheme którą zwraca nasz serwis (w naszym przypadku nie jest to potrzebne ponieważ zwracamy typ ActionResult<BookDto>>):

    [HttpGet("{id:int}")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<BookDto>> GetBook(int id)
    {
        if (id <= 0)
        {
            return BadRequest();
        }
        //...code, code...
    }


Uruchamiając ponownie aplikacje z powyższą modyfikacją informacja Undocumented nie pojawi się ponownie. Dodatkowo poniżej otrzymamy listę wszystkich możliwych kodów rezultatów




Różnica między ControllerBase i Controller (MVC vs API)

Tworząc aplikację MVC Core lub API Core możemy nasz Controller dziedziczyć z jednej z dwóch klas ControllerBase lub Controller

Ogólna zasada dziedziczenia jest następująca:

  • ControllerBase - klasa bazowa dla projektów API
  • Controller - klasa bazowa dla projektów MVC
Klasa Controller zawiera składowe specyficzne dla widoków MVC. Zdecydowanie w API nie będą one przydatne. Przykładowo udostępnia takie właściwości jak ViewBag, ViewData, TempData, itp.

Wyjątkiem jest sytuacja, kiedy zamierzamy wykorzystać te same Controllery w aplikacji MVC i API. Dziedziczenie po Controller jest wtedy uzasadnione.  

    public class ApiController : ControllerBase
    {

    }

    public class MvcController : Controller
    {

    }

niedziela, 4 grudnia 2022

API ASP.NET Core - Walidacja danych wejściowych

 Dane wejściowe w API możemy sprawdzać na dwa sposoby:

  1. Korzystając z atrybutów walidacji na modelu
  2. Tworząc własną logikę walidacji w klasie Controllera

1. Stworzenie walidacji jako atrybutu modelu

Jako przykład możemy stworzyć atrybut, który sprawdzi, że wszystkie znaki ciągu teksu są wielkimi literami:

using System.ComponentModel.DataAnnotations;
using System.Globalization;
using static System.String;

namespace BookStoreApi;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class UpperCaseAttribute : ValidationAttribute
{
    public UpperCaseAttribute() : base("All characters for {0} field must be upper case")
    {
    }

    public override bool IsValid(object? value)
    {
        return (value is string str) && str.All(char.IsUpper);
    }

    public override string FormatErrorMessage(string name)
    {
        return Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
    }
}


Ważniejsze punkty implementacji
  • klasa którą implementuje powinna dziedziczyć po ValidationAttribute
  • W konstruktorze możemy (anie nie musimy) przekazać error message, który zostanie wyświetlony użytkownikowi jeżeli walidacja się nie powiedzie. Możemy także zostawić miejsce na nazwę pola, która zostanie wstrzyknięta do wiadomości błędu - {0}
  • Metoda FormatErrorMessage zawiera parametr name, który reprezentuje sprawdzane pole
Atrybut dodajemy na pole naszego obiektu DTO i możemy sprawdzić jego działanie:

public class BookDto
{
    public int Id { get; set; }

    [Required, MaxLength(250), MinLength(3)]
    public string Name { get; set; }

    [Required, MaxLength(250), MinLength(3), UpperCase]
    public string Author { get; set; }

    public int PublishYear { get; set; }
}



2. Własna logika w klasie Controllera

Logikę walidacji można także zawrzeć bezpośrednio w klasie Controllera:

    public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookDto bookDto)
    {
        if (bookDto == null)
        {
            return BadRequest(bookDto);
        }

        if (bookDto.Id > 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError);
        }

        if (!bookDto.Author.All(char.IsUpper))
        {
            ModelState.AddModelError("AuthorValidation", "All characters for Author field must be upper case ccc");
            return BadRequest(ModelState);
        }
        //... code....
     }
Po uruchomieniu aplikacji i przetestowaniu, analogicznie jak poprzednio otrzymamy błąd w przypadku gdy pole Author będzie zawierało jakiekolwiek małe litery. 

Obydwa sposoby walidacji są jak najbardziej poprawne. Drugi z jednej strony może wydawać się mniej skomplikowany (mniej kodu), jednak stworzenie atrybutu pozwala na jego łatwe re-używanie.

poniedziałek, 7 września 2020

OWASP 2017 - 10 najczęstszych zagrożeń aplikacji internetowych (web) - A3 - A10

Kolejnymi zagrożeniami na liscie OWASP 2017 są:

A3. Sensitive Data Exposure

Powszechne zagrożenie dla aplikacji internetowych - wyciek danych. Dane te to np. numery PESEL, karty kredytowe etc. Najczęstszą przyczyną wycieków tych danych są słabe zabezpieczenia bazy danych, brak szyfrowania danych czy też użycie słabych algorytmów szyfrowania. 
W jaki sposób zapobiec wyciekowi poufnych danych?
  • ogranicz ilość przechowywanych danych. Być może nie potrzebujesz ich w ogóle w aplikacji? Jeżeli jednak są wymagane do poprawnego działania systemu sprawdź regulacje prawne 
  • szyfruj dane oraz użyj bezpiecznych protokołów (TLS)
  • szyfruj hasła używając silnych algorytmów (Argon2, PBKDF2). Unikaj stosowania MD5, SHA1, SHA256. Bądź na bieżąco z obecnymi standardami - algorytmy zmieniają się, wymyślane są nowe
  • kiedy przeglądarka wyświetla poufne dane dodaj dyrektywy, które zapobiegną użycia cache dla poufnych informacji

A4. XML External Entities (XXE)

External Entities to elementy pozwalające budować XML w sposób dynamiczny. Podczas przetwarzania XMLa umożliwiają wstrzykiwanie definicji elementów z zewnętrznych źródeł.
Kiedy aplikacja jest narażona na ten typ ataku?
  • aplikacja jako dane wejściowe przyjmuje bezpośrednio XML
  • użycie SOAP < 1.2
  • SOAPowy serwis używa DTD (document type definitions)

A5. Broken access control

Atakujący uzyskuje dostęp do funkcjonalności, do których normalnie nie miałby dostępu. Może się to odbyć np. poprzez modyfikację adresu url strony i uzyskanie dostępu do panelu administracyjnego, bądź też funkcji która jest normalnie widoczna tylko dla moderatora/administratora. 
W jaki sposób bronić się przed tym typem ataku?
  • warstwa autoryzacji powinna być zaimplementowana w jednym komponencie i reużywana przez wszystkie inne składowe systemu
  • domyślnie zawsze zabraniamy dostępu do danego zasobu
  • logujemy błędne próby logowania i analizujmy incydenty związane z wieloma próbami zalogowania się przy użyciu błędnych uwierzytelnień 
  • sesja użytkownika powinna być prawidłowo kończona po jego wylogowaniu z systemu

A6. Security misconfiguration

Ten typ zagrożenia wynika z stosowania domyślnych kont administratora, pozostawianiu niezabezpieczonych folderów na serwerze itp. Atakujący może w ten sposób dowiedzieć się więcej o systemie i poznać jego słabe punkty. Zagrożenia tego typu możemy spodziewać się na każdym poziomie: sieć, system, framework, aplikacja. 
W jaki sposób zapobiegać tego typu atakom?
  • automatyzacja manualnej konfiguracji (zwłaszcza powtarzalnej konfiguracji)
  • środowiska (testowe, dweloperskie i produkcyjne) powinny mieć taka samą konfigurację
  • nie instalujemy na serwerach zbędnego oprogramowania
  • cyklicznie aktualizujemy system serwera i oprogramowanie na nim zainstalowane

A7. Cross-Site Scripting (XSS)

Atak ten polega na umieszczeniu niebezpiecznego kodu w treści strony. Nieświadomy użytkownik wchodząc na stronę uruchamia niepożądany kod infekując swój komputer. XSS dzielimy na trzy typy: Reflected XSS, Stored XSS, DOM based XSS. 
Jak się zabezpieczyć przed tego typu atakiem?
  • używajmy gotowych narzędzi w frameworkach
  • poznajmy słabe strony frameworka, który używamy
  • sprawdzamy dane pochodzące od użytkownika
  • unikamy pracy z czystym HTMLem/JSptem

A8. Insecure deserialization

Zagrożenie to polega na wprowadzeniu do zserializowanej wiadomości nieuprawnionej treści. Treść ta może wpłynąć na sposób przetwarzania kodu, wywołać funkcje, których w normalnym procesowaniu byśmy nie wywołali. 
Najlepszym sposobem obrony przed tym typem ataku jest nie przyjmowanie zserialozowanej treści z nieznanych źródeł. Innym sposobem walki jest deserializacja obiektów w odizolowanym środowisku z bardzo niskimi uprawnieniami. Nawet jeżeli do naszego systemu przedostanie się niechciany pakiet danych nie będzie on miał uprawnień do wykonania niebezpiecznych operacji. Monitorowania i logowanie błędów deserializacji - bardzo często da nam to informacje z jakiego źródła pochodzą podejrzane wiadomości. 

A9. Using components with known vulnerabilities

Tu sprawa wydaje się dosyć prosta. Oprogramowanie zmienia się. Frameworki zmieniaja się. Wykrywane są nowe błędu i publikowane poprawki do nich. Uruchamiając aplikację w środowisku produkcyjnym musimy mieć świadomość, że po pewnym czasie będziemy musieli uaktualnić działające na nim oprogramowanie, framworki, bazy danych itp. 
Dobrym sposobem zabezpieczenia jest także ograniczenie ilości oprogramowania uruchomionego na środowisku produkcyjnym - instalujemy tylko to co jest niezbędne. Warto także prowadzić i przestrzegać harmonogramu uaktualniania oprogramowania co pozwoli być na bieżąco ze wszystkimi aktualizacjami. Jeżeli instalujemy oprogramowanie - używajmy oficjalnych kanałów. Warto także monitorować serwisy poświęcone tematyce bezpieczeństwa np. https://www.cvedetails.com/version-search.php. 

A10. Insufficient logging and monitoring
To przykład w jaki sposób pominięcie dobrego monitorowania i logowania może zagrozić naszemu systemowi. Atakujący wręcz liczy, że przez źle zaimplementowany monitoring zyska czas potrzebny do przeprowadzenia skutecznego ataku. Można też pójść w drugą stronę - logować zbyt wiele co prowadzi do kompletnego chaosu i uniemożliwia analizę logów. 
Dobre logowanie przechwyci z pewnością wszelkie próby logowania do systemu z błędnymi poświadczeniami (dane te następnie możemy wykorzystać i zablokować potencjalnego atakującego). Logowanie powinno być spójne i przechowywane w centralnym miejscu umożliwiającym analizę danych. Możemy na przykład skorzystać z ELK bądź Splunk w celu magazynowania i przetwarzania logów. Oprócz logowania musimy zadbać też o odpowiednie powiadamianie.