diff --git a/grails-app/assets/javascripts/application.js b/grails-app/assets/javascripts/application.js new file mode 100644 index 000000000..a0d04bf3a --- /dev/null +++ b/grails-app/assets/javascripts/application.js @@ -0,0 +1,129 @@ +//= require angular/angular.min +//= require angular/angular-route.min +//= require angular/angular-cookie.min +//= require angular/angular-resource.min +//= require angular/ng-grid-2.0.11/ng-grid.min +//= require angular/ui-bootstrap-tpls-0.6.0.min +//= require angular/ui-bootstrap-0.7.0.min +//= require_directory . + +//= require_self + +var meta = document.querySelector('meta[name="_csrf"]'); +var csrfToken = meta ? meta.getAttribute('content') : ''; +var csrfHeader = document.querySelector('meta[name="_csrf_header"]'); +var csrfHeaderName = csrfHeader ? csrfHeader.getAttribute('content') : 'X-CSRF-TOKEN'; + +var app = angular.module('streama', [ + 'ngRoute', 'ngResource', 'ngGrid', 'ui.bootstrap', 'ngCookies' +]); + +app.config(['$httpProvider', function($httpProvider) { + $httpProvider.interceptors.push(function() { + return { + request: function(config) { + if (csrfToken) { + config.headers[csrfHeaderName] = csrfToken; + } + return config; + } + }; + }); +}]); + +app.factory('ApiService', ['$resource', function($resource) { + return { + shows: $resource('/api/show/:id', {id: '@id'}), + episodes: $resource('/api/episode/:id', {id: '@id'}), + movies: $resource('/api/movie/:id', {id: '@id'}), + files: $resource('/api/file/:id', {id: '@id'}), + genres: $resource('/api/genre/:id', {id: '@id'}), + tags: $resource('/api/tag/:id', {id: '@id'}) + }; +}]); + +app.factory('SocketService', function() { + var sockets = {}; + return { + getSocket: function(namespace) { + if (!sockets[namespace]) { + sockets[namespace] = io.connect(namespace); + } + return sockets[namespace]; + } + }; +}); + +app.controller('NavController', function($scope, $http, $location, ApiService) { + $scope.navItems = [ + {label: 'Home', path: '/'}, + {label: 'Shows', path: '/shows'}, + {label: 'Movies', path: '/movies'}, + {label: 'Upload', path: '/upload'}, + {label: 'Admin', path: '/admin'} + ]; + + $scope.isActive = function(path) { + return $location.path() === path; + }; +}); + +app.controller('ShowController', function($scope, ApiService) { + $scope.shows = ApiService.shows.query(); +}); + +app.controller('MovieController', function($scope, ApiService) { + $scope.movies = ApiService.movies.query(); +}); + +app.controller('UploadController', function($scope, $http) { + $scope.uploadFile = function() { + var formData = new FormData(); + formData.append('file', $scope.file); + $http.post('/api/upload', formData, { + headers: {'Content-Type': undefined} + }).then(function(response) { + $scope.message = 'Upload successful'; + }).catch(function(error) { + $scope.error = 'Upload failed'; + }); + }; +}); + +app.config(['$routeProvider', function($routeProvider) { + $routeProvider + .when('/', { + templateUrl: '/assets/partials/home.html', + controller: 'HomeController' + }) + .when('/shows', { + templateUrl: '/assets/partials/shows.html', + controller: 'ShowController' + }) + .when('/movies', { + templateUrl: '/assets/partials/movies.html', + controller: 'MovieController' + }) + .when('/upload', { + templateUrl: '/assets/partials/upload.html', + controller: 'UploadController' + }) + .when('/admin', { + templateUrl: '/assets/partials/admin.html', + controller: 'AdminController' + }) + .otherwise({ + redirectTo: '/' + }); +}]); + +app.controller('HomeController', function($scope, ApiService) { + $scope.featuredShows = ApiService.shows.query({featured: true}); + $scope.featuredMovies = ApiService.movies.query({featured: true}); +}); + +app.controller('AdminController', function($scope, ApiService) { + $scope.shows = ApiService.shows.query(); + $scope.movies = ApiService.movies.query(); + $scope.genres = ApiService.genres.query(); +}); \ No newline at end of file diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index f000ff41f..4ff47d276 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -101,6 +101,12 @@ spring: groovy: template: check-template-location: false + security: + csrf: + enabled: true + headerName: 'X-CSRF-TOKEN' + filter: + enabled: true --- grails: diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy index 0969fb25d..1f4dc3b6b 100644 --- a/grails-app/conf/spring/resources.groovy +++ b/grails-app/conf/spring/resources.groovy @@ -1,8 +1,32 @@ +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.csrf.CookieCsrfTokenRepository import org.springframework.web.client.RestTemplate import streama.LdapUserDetailsContextMapper // Place your Spring DSL code here beans = { + + securityFilterChain(SecurityFilterChain) { bean -> + bean.factoryMethod = 'securityFilterChain' + bean.parent = '' + bean.autowireMode = 2 + } + + static securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf() + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .ignoringAntMatchers('/api/**', '/web-api/**') + .and() + .authorizeRequests() + .antMatchers('/assets/**', '/**/assets/**').permitAll() + .anyRequest().authenticated() + return http.build() + } ldapUserDetailsMapper(LdapUserDetailsContextMapper) { } diff --git a/grails-app/controllers/auth/CsrfController.groovy b/grails-app/controllers/auth/CsrfController.groovy new file mode 100644 index 000000000..95bdfea13 --- /dev/null +++ b/grails-app/controllers/auth/CsrfController.groovy @@ -0,0 +1,14 @@ +package auth + +import grails.plugin.springweb.Security-conscious +import org.springframework.security.web.csrf.CsrfToken +import org.springframework.web.context.request.RequestContextHolder + +@Security-conscious +class CsrfController { + + def index() { + CsrfToken token = RequestContextHolder.requestAttributes.csrfToken + render([token: token?.token ?: ''] as JSON) + } +} diff --git a/grails-app/services/auth/CsrfTokenService.groovy b/grails-app/services/auth/CsrfTokenService.groovy new file mode 100644 index 000000000..7618d81ac --- /dev/null +++ b/grails-app/services/auth/CsrfTokenService.groovy @@ -0,0 +1,64 @@ +package auth + +import grails.core.GrailsApplication +import grails.util.Holders + +class CsrfTokenService { + + static final String TOKEN_ATTR = 'csrfToken' + static final int TOKEN_LENGTH = 32 + + GrailsApplication grailsApplication + + String generateToken() { + def session = getSession() + if (!session) { + return null + } + String token = generateSecureToken() + session.setAttribute(TOKEN_ATTR, token) + return token + } + + String getCurrentToken() { + def session = getSession() + if (!session) { + return null + } + return session.getAttribute(TOKEN_ATTR) + } + + boolean validateToken(String token) { + if (!token) { + return false + } + def session = getSession() + if (!session) { + return false + } + String storedToken = session.getAttribute(TOKEN_ATTR) + return token == storedToken + } + + void invalidateToken() { + def session = getSession() + if (session) { + session.removeAttribute(TOKEN_ATTR) + } + } + + protected String generateSecureToken() { + SecureRandom random = new SecureRandom() + byte[] bytes = new byte[TOKEN_LENGTH] + random.nextBytes(bytes) + return bytes.encodeBase64Url().toString() + } + + protected def getSession() { + try { + return Holders.getGrailsWebRequest()?.getSession() + } catch (Exception e) { + return null + } + } +}